forked from StrafesNET/map-tool
Compare commits
26 Commits
feature/ex
...
master
Author | SHA1 | Date | |
---|---|---|---|
c415ffbdab | |||
cbc818bd03 | |||
53d2f7a5e8 | |||
96d1cc87a2 | |||
5915dd730f | |||
e626131d95 | |||
69ffbf4837 | |||
167be8f587 | |||
e92528ad83 | |||
8e9c76d6f8 | |||
a5c48d4684 | |||
1b5eec9eaf | |||
ef5703f282 | |||
9685301b30 | |||
d2b455c87b | |||
9de2790cc8 | |||
47e93325ad | |||
de9712b7a1 | |||
c2d0a4487c | |||
dc9fd2c442 | |||
4199d41d3f | |||
7fbcb206ff | |||
a17901d473 | |||
b88c6b899a | |||
835d4bbecd | |||
b756dc979c |
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
@ -0,0 +1,2 @@
|
||||
[registries.strafesnet]
|
||||
index = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"
|
1126
Cargo.lock
generated
1126
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
22
Cargo.toml
22
Cargo.toml
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "map-tool"
|
||||
version = "1.3.0"
|
||||
name = "mapfixer"
|
||||
version = "1.1.1"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@ -9,15 +9,13 @@ edition = "2021"
|
||||
anyhow = "1.0.75"
|
||||
clap = { version = "4.4.2", features = ["derive"] }
|
||||
flate2 = "1.0.27"
|
||||
image = "0.24.7"
|
||||
image_dds = "0.1.1"
|
||||
lazy-regex = "3.1.0"
|
||||
rbx_binary = "0.7.1"
|
||||
rbx_dom_weak = "2.5.0"
|
||||
rbx_reflection_database = "0.2.7"
|
||||
rbx_xml = "0.13.1"
|
||||
rbx_binary = { version = "0.7.4", registry = "strafesnet"}
|
||||
rbx_dom_weak = { version = "2.7.0", registry = "strafesnet"}
|
||||
rbx_reflection_database = { version = "0.2.10", registry = "strafesnet"}
|
||||
rbx_xml = { version = "0.13.3", registry = "strafesnet"}
|
||||
|
||||
#[profile.release]
|
||||
#lto = true
|
||||
#strip = true
|
||||
#codegen-units = 1
|
||||
[profile.release]
|
||||
lto = true
|
||||
strip = true
|
||||
codegen-units = 1
|
||||
|
28
LICENSE
28
LICENSE
@ -1,9 +1,23 @@
|
||||
MIT License
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
Copyright (c) 2023 StrafesNET Map Tool Developers
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
582
src/main.rs
582
src/main.rs
@ -1,4 +1,4 @@
|
||||
use std::io::{Read, Seek};
|
||||
use std::{io::{Read, Seek}, path::PathBuf};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use anyhow::Result as AResult;
|
||||
|
||||
@ -6,28 +6,24 @@ use anyhow::Result as AResult;
|
||||
#[command(author, version, about, long_about = None)]
|
||||
#[command(propagate_version = true)]
|
||||
struct Cli {
|
||||
#[arg(long)]
|
||||
path:Option<PathBuf>,
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
Download(MapList),
|
||||
DownloadTextures(PathBufList),
|
||||
ConvertTextures,
|
||||
DownloadMeshes(PathBufList),
|
||||
Extract(PathBufList),
|
||||
WriteAttributes,
|
||||
ExtractScripts(PathBufList),
|
||||
Interactive,
|
||||
Replace,
|
||||
Scan,
|
||||
UnzipAll,
|
||||
Upload,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
struct PathBufList {
|
||||
paths:Vec<std::path::PathBuf>
|
||||
paths:Vec<PathBuf>
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
@ -35,6 +31,17 @@ struct MapList {
|
||||
maps: Vec<u64>,
|
||||
}
|
||||
|
||||
fn main() -> AResult<()> {
|
||||
let cli = Cli::parse();
|
||||
match cli.command {
|
||||
Commands::ExtractScripts(pathlist)=>extract_scripts(pathlist.paths),
|
||||
Commands::Interactive=>interactive(),
|
||||
Commands::Replace=>replace(),
|
||||
Commands::Scan=>scan(),
|
||||
Commands::Upload=>upload(),
|
||||
}
|
||||
}
|
||||
|
||||
fn class_is_a(class: &str, superclass: &str) -> bool {
|
||||
if class==superclass {
|
||||
return true
|
||||
@ -45,7 +52,7 @@ fn class_is_a(class: &str, superclass: &str) -> bool {
|
||||
return class_is_a(&class_super, superclass)
|
||||
}
|
||||
}
|
||||
return false
|
||||
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() {
|
||||
@ -57,16 +64,6 @@ fn recursive_collect_superclass(objects: &mut std::vec::Vec<rbx_dom_weak::types:
|
||||
}
|
||||
}
|
||||
}
|
||||
fn recursive_collect_regex(objects: &mut std::vec::Vec<rbx_dom_weak::types::Ref>,dom: &rbx_dom_weak::WeakDom, instance: &rbx_dom_weak::Instance, regex: &lazy_regex::Lazy<lazy_regex::Regex>){
|
||||
for &referent in instance.children() {
|
||||
if let Some(c) = dom.get_by_ref(referent) {
|
||||
if regex.captures(c.name.as_str()).is_some(){
|
||||
objects.push(c.referent());//copy ref
|
||||
}
|
||||
recursive_collect_regex(objects,dom,c,regex);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn get_full_name(dom:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance) -> String{
|
||||
let mut full_name=instance.name.clone();
|
||||
let mut pref=instance.parent();
|
||||
@ -78,8 +75,6 @@ fn get_full_name(dom:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance) ->
|
||||
full_name
|
||||
}
|
||||
|
||||
//download
|
||||
//download list of maps to maps/unprocessed
|
||||
//scan (scripts)
|
||||
//iter maps/unprocessed
|
||||
//passing moves to maps/verified
|
||||
@ -87,9 +82,6 @@ fn get_full_name(dom:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance) ->
|
||||
//replace (edits & deletions)
|
||||
//iter maps/blocked
|
||||
//replace scripts and put in maps/unprocessed
|
||||
//upload
|
||||
//iter maps/verified
|
||||
//interactively print DisplayName/Creator and ask for target upload ids
|
||||
//interactive
|
||||
//iter maps/unprocessed
|
||||
//for each unique script, load it into the file current.lua and have it open in sublime text
|
||||
@ -101,28 +93,6 @@ fn get_script_refs(dom:&rbx_dom_weak::WeakDom) -> Vec<rbx_dom_weak::types::Ref>{
|
||||
recursive_collect_superclass(&mut scripts, dom, dom.root(),"LuaSourceContainer");
|
||||
scripts
|
||||
}
|
||||
fn get_button_refs(dom:&rbx_dom_weak::WeakDom) -> Vec<rbx_dom_weak::types::Ref>{
|
||||
let mut buttons = std::vec::Vec::new();
|
||||
recursive_collect_regex(&mut buttons, dom, dom.root(),lazy_regex::regex!(r"Button(\d+)$"));
|
||||
buttons
|
||||
}
|
||||
fn get_texture_refs(dom:&rbx_dom_weak::WeakDom) -> Vec<rbx_dom_weak::types::Ref>{
|
||||
let mut objects = std::vec::Vec::new();
|
||||
recursive_collect_superclass(&mut objects, dom, dom.root(),"Decal");
|
||||
//get ids
|
||||
//clear vec
|
||||
//next class
|
||||
objects
|
||||
}
|
||||
fn get_mesh_refs(dom:&rbx_dom_weak::WeakDom) -> Vec<rbx_dom_weak::types::Ref>{
|
||||
let mut objects = std::vec::Vec::new();
|
||||
recursive_collect_superclass(&mut objects, dom, dom.root(),"FileMesh");
|
||||
recursive_collect_superclass(&mut objects, dom, dom.root(),"MeshPart");
|
||||
//get ids
|
||||
//clear vec
|
||||
//next class
|
||||
objects
|
||||
}
|
||||
|
||||
enum ReaderType<'a, R:Read+Seek>{
|
||||
GZip(flate2::read::GzDecoder<&'a mut R>),
|
||||
@ -147,8 +117,8 @@ fn load_dom<R:Read+Seek>(input:&mut R)->AResult<rbx_dom_weak::WeakDom>{
|
||||
match &first_8[0..4]{
|
||||
b"<rob"=>{
|
||||
match &first_8[4..8]{
|
||||
b"lox!"=>return rbx_binary::from_reader(input).map_err(anyhow::Error::msg),
|
||||
b"lox "=>return rbx_xml::from_reader(input,rbx_xml::DecodeOptions::default()).map_err(anyhow::Error::msg),
|
||||
b"lox!"=>rbx_binary::from_reader(input).map_err(anyhow::Error::msg),
|
||||
b"lox "=>rbx_xml::from_reader(input,rbx_xml::DecodeOptions::default()).map_err(anyhow::Error::msg),
|
||||
other=>Err(anyhow::Error::msg(format!("Unknown Roblox file type {:?}",other))),
|
||||
}
|
||||
},
|
||||
@ -231,7 +201,7 @@ fn find_first_child_class<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&'a rbx_dom
|
||||
None
|
||||
}
|
||||
|
||||
fn get_mapinfo(dom:&rbx_dom_weak::WeakDom) -> AResult<(String,String,String)>{
|
||||
fn get_mapinfo(dom:&rbx_dom_weak::WeakDom) -> AResult<(String,String,String,rbx_dom_weak::types::Ref)>{
|
||||
let workspace_children=dom.root().children();
|
||||
if workspace_children.len()!=1{
|
||||
return Err(anyhow::Error::msg("there can only be one model"));
|
||||
@ -245,34 +215,11 @@ fn get_mapinfo(dom:&rbx_dom_weak::WeakDom) -> AResult<(String,String,String)>{
|
||||
creator.properties.get("Value"),
|
||||
displayname.properties.get("Value")
|
||||
){
|
||||
return Ok((model_instance.name.clone(),creator_string.clone(),displayname_string.clone()));
|
||||
return Ok((model_instance.name.clone(),creator_string.clone(),displayname_string.clone(),displayname.referent()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return Err(anyhow::Error::msg("no stuff in map"));
|
||||
}
|
||||
|
||||
fn download(map_list: Vec<u64>) -> AResult<()>{
|
||||
let header=format!("Cookie: .ROBLOSECURITY={}",std::env::var("RBXCOOKIE")?);
|
||||
let shared_args=&[
|
||||
"-q",
|
||||
"--header",
|
||||
header.as_str(),
|
||||
"-O",
|
||||
];
|
||||
let processes_result:Result<Vec<_>, _>=map_list.iter().map(|map_id|{
|
||||
std::process::Command::new("wget")
|
||||
.args(shared_args)
|
||||
.arg(format!("maps/unprocessed/{}.rbxm",map_id))
|
||||
.arg(format!("https://assetdelivery.roblox.com/v1/asset/?ID={}",map_id))
|
||||
.spawn()
|
||||
}).collect();
|
||||
//naively wait for all because idk how to make an async progress bar lmao
|
||||
for child in processes_result?{
|
||||
let output=child.wait_with_output()?;
|
||||
println!("map exit_success:{}",output.status.success());
|
||||
}
|
||||
Ok(())
|
||||
Err(anyhow::Error::msg("no stuff in map"))
|
||||
}
|
||||
|
||||
struct RobloxAssetId(u64);
|
||||
@ -291,213 +238,6 @@ impl std::str::FromStr for RobloxAssetId {
|
||||
Err(RobloxAssetIdParseErr)
|
||||
}
|
||||
}
|
||||
/* The ones I'm interested in:
|
||||
Beam.Texture
|
||||
Decal.Texture
|
||||
FileMesh.MeshId
|
||||
FileMesh.TextureId
|
||||
MaterialVariant.ColorMap
|
||||
MaterialVariant.MetalnessMap
|
||||
MaterialVariant.NormalMap
|
||||
MaterialVariant.RoughnessMap
|
||||
MeshPart.MeshId
|
||||
MeshPart.TextureID
|
||||
ParticleEmitter.Texture
|
||||
Sky.MoonTextureId
|
||||
Sky.SkyboxBk
|
||||
Sky.SkyboxDn
|
||||
Sky.SkyboxFt
|
||||
Sky.SkyboxLf
|
||||
Sky.SkyboxRt
|
||||
Sky.SkyboxUp
|
||||
Sky.SunTextureId
|
||||
SurfaceAppearance.ColorMap
|
||||
SurfaceAppearance.MetalnessMap
|
||||
SurfaceAppearance.NormalMap
|
||||
SurfaceAppearance.RoughnessMap
|
||||
SurfaceAppearance.TexturePack
|
||||
*/
|
||||
fn download_textures(paths: Vec<std::path::PathBuf>) -> AResult<()>{
|
||||
println!("download_textures paths:{:?}",paths);
|
||||
let header=format!("Cookie: .ROBLOSECURITY={}",std::env::var("RBXCOOKIE")?);
|
||||
let shared_args=&[
|
||||
"-q",
|
||||
"--header",
|
||||
header.as_str(),
|
||||
"-O",
|
||||
];
|
||||
let mut texture_list=std::collections::HashSet::new();
|
||||
for path in paths {
|
||||
let mut input = std::io::BufReader::new(std::fs::File::open(path.clone())?);
|
||||
|
||||
match get_dom(&mut input){
|
||||
Ok(dom)=>{
|
||||
let object_refs = get_texture_refs(&dom);
|
||||
for &object_ref in object_refs.iter() {
|
||||
if let Some(object)=dom.get_by_ref(object_ref){
|
||||
if let Some(rbx_dom_weak::types::Variant::Content(content)) = object.properties.get("Texture") {
|
||||
println!("Texture content:{:?}",content);
|
||||
if let Ok(asset_id)=content.clone().into_string().parse::<RobloxAssetId>(){
|
||||
texture_list.insert(asset_id.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e)=>println!("error loading map {:?}: {:?}",path.file_name(),e),
|
||||
}
|
||||
}
|
||||
println!("Texture list:{:?}",texture_list);
|
||||
let processes_result:Result<Vec<_>, _>=texture_list.iter().map(|asset_id|{
|
||||
std::process::Command::new("wget")
|
||||
.args(shared_args)
|
||||
.arg(format!("textures/unprocessed/{}",asset_id))
|
||||
.arg(format!("https://assetdelivery.roblox.com/v1/asset/?ID={}",asset_id))
|
||||
.spawn()
|
||||
}).collect();
|
||||
//naively wait for all because idk how to make an async progress bar lmao
|
||||
for child in processes_result?{
|
||||
let output=child.wait_with_output()?;
|
||||
println!("texture exit_success:{}",output.status.success());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn download_meshes(paths: Vec<std::path::PathBuf>) -> AResult<()>{
|
||||
println!("download_meshes paths:{:?}",paths);
|
||||
let header=format!("Cookie: .ROBLOSECURITY={}",std::env::var("RBXCOOKIE")?);
|
||||
let shared_args=&[
|
||||
"-q",
|
||||
"--header",
|
||||
header.as_str(),
|
||||
"-O",
|
||||
];
|
||||
let mut mesh_list=std::collections::HashSet::new();
|
||||
for path in paths {
|
||||
let mut input = std::io::BufReader::new(std::fs::File::open(path.clone())?);
|
||||
|
||||
match get_dom(&mut input){
|
||||
Ok(dom)=>{
|
||||
let object_refs = get_mesh_refs(&dom);
|
||||
for &object_ref in object_refs.iter() {
|
||||
if let Some(object)=dom.get_by_ref(object_ref){
|
||||
if let Some(rbx_dom_weak::types::Variant::Content(content)) = object.properties.get("MeshId") {
|
||||
println!("Mesh content:{:?}",content);
|
||||
if let Ok(asset_id)=content.clone().into_string().parse::<RobloxAssetId>(){
|
||||
mesh_list.insert(asset_id.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e)=>println!("error loading map {:?}: {:?}",path.file_name(),e),
|
||||
}
|
||||
}
|
||||
println!("Mesh list:{:?}",mesh_list);
|
||||
let processes_result:Result<Vec<_>, _>=mesh_list.iter().map(|asset_id|{
|
||||
std::process::Command::new("wget")
|
||||
.args(shared_args)
|
||||
.arg(format!("meshes/unprocessed/{}",asset_id))
|
||||
.arg(format!("https://assetdelivery.roblox.com/v1/asset/?ID={}",asset_id))
|
||||
.spawn()
|
||||
}).collect();
|
||||
//naively wait for all because idk how to make an async progress bar lmao
|
||||
for child in processes_result?{
|
||||
let output=child.wait_with_output()?;
|
||||
println!("Mesh exit_success:{}",output.status.success());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_image<R:Read+Seek+std::io::BufRead>(input:&mut R)->AResult<image::DynamicImage>{
|
||||
let mut fourcc=[0u8;4];
|
||||
input.read_exact(&mut fourcc)?;
|
||||
input.rewind()?;
|
||||
match &fourcc{
|
||||
b"\x89PNG"=>Ok(image::load(input,image::ImageFormat::Png)?),
|
||||
b"\xFF\xD8\xFF\xE0"=>Ok(image::load(input,image::ImageFormat::Jpeg)?),//JFIF
|
||||
b"<rob"=>Err(anyhow::Error::msg("Roblox xml garbage is not supported yet")),
|
||||
other=>Err(anyhow::Error::msg(format!("Unknown texture format {:?}",other))),
|
||||
}
|
||||
}
|
||||
|
||||
fn convert(file_thing:std::fs::DirEntry) -> AResult<()>{
|
||||
let mut input = std::io::BufReader::new(std::fs::File::open(file_thing.path())?);
|
||||
|
||||
let mut extracted_input=None;
|
||||
let image=match maybe_gzip_decode(&mut input){
|
||||
Ok(ReaderType::GZip(mut readable)) => {
|
||||
//gzip
|
||||
let mut extracted:Vec<u8>=Vec::new();
|
||||
//read the entire thing to the end so that I can clone the data and write a png to processed images
|
||||
readable.read_to_end(&mut extracted)?;
|
||||
extracted_input=Some(extracted.clone());
|
||||
load_image(&mut std::io::Cursor::new(extracted))
|
||||
},
|
||||
Ok(ReaderType::Raw(readable)) => load_image(readable),
|
||||
Err(e) => Err(e)?,
|
||||
}?.to_rgba8();//this sets a=255, arcane is actually supposed to look like that
|
||||
|
||||
let format=if image.width()%4!=0||image.height()%4!=0{
|
||||
image_dds::ImageFormat::R8G8B8A8Srgb
|
||||
}else{
|
||||
image_dds::ImageFormat::BC7Srgb
|
||||
};
|
||||
//this fails if the image dimensions are not a multiple of 4
|
||||
let dds = image_dds::dds_from_image(
|
||||
&image,
|
||||
format,
|
||||
image_dds::Quality::Slow,
|
||||
image_dds::Mipmaps::GeneratedAutomatic,
|
||||
)?;
|
||||
|
||||
//write dds
|
||||
let mut dest=std::path::PathBuf::from("textures/dds");
|
||||
dest.push(file_thing.file_name());
|
||||
dest.set_extension("dds");
|
||||
let mut writer = std::io::BufWriter::new(std::fs::File::create(dest)?);
|
||||
dds.write(&mut writer)?;
|
||||
|
||||
if let Some(mut extracted)=extracted_input{
|
||||
//write extracted to processed
|
||||
let mut dest=std::path::PathBuf::from("textures/processed");
|
||||
dest.push(file_thing.file_name());
|
||||
std::fs::write(dest, &mut extracted)?;
|
||||
//delete ugly gzip file
|
||||
std::fs::remove_file(file_thing.path())?;
|
||||
}else{
|
||||
//move file to processed
|
||||
let mut dest=std::path::PathBuf::from("textures/processed");
|
||||
dest.push(file_thing.file_name());
|
||||
std::fs::rename(file_thing.path(), dest)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn convert_textures() -> AResult<()>{
|
||||
let start = std::time::Instant::now();
|
||||
let mut threads=Vec::new();
|
||||
for entry in std::fs::read_dir("textures/unprocessed")? {
|
||||
let file_thing=entry?;
|
||||
threads.push(std::thread::spawn(move ||{
|
||||
let file_name=format!("{:?}",file_thing);
|
||||
let result=convert(file_thing);
|
||||
if let Err(e)=result{
|
||||
println!("error processing file:{:?} error message:{:?}",file_name,e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
let mut i=0;
|
||||
let n_threads=threads.len();
|
||||
for thread in threads{
|
||||
i+=1;
|
||||
if let Err(e)=thread.join(){
|
||||
println!("thread error: {:?}",e);
|
||||
}else{
|
||||
println!("{}/{}",i,n_threads);
|
||||
}
|
||||
}
|
||||
println!("{:?}", start.elapsed());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
enum Scan{
|
||||
Passed,
|
||||
@ -550,12 +290,12 @@ fn scan() -> AResult<()>{
|
||||
}
|
||||
}
|
||||
let mut dest=match fail_type {
|
||||
Scan::Passed => std::path::PathBuf::from("maps/processed"),
|
||||
Scan::Passed => PathBuf::from("maps/processed"),
|
||||
Scan::Blocked => {
|
||||
println!("{:?} - {} {} not allowed.",file_thing.file_name(),fail_count,if fail_count==1 {"script"}else{"scripts"});
|
||||
std::path::PathBuf::from("maps/blocked")
|
||||
PathBuf::from("maps/blocked")
|
||||
}
|
||||
Scan::Flagged => std::path::PathBuf::from("maps/flagged")
|
||||
Scan::Flagged => PathBuf::from("maps/flagged")
|
||||
};
|
||||
dest.push(file_thing.file_name());
|
||||
std::fs::rename(file_thing.path(), dest)?;
|
||||
@ -564,7 +304,7 @@ fn scan() -> AResult<()>{
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract(paths: Vec<std::path::PathBuf>) -> AResult<()>{
|
||||
fn extract_scripts(paths: Vec<PathBuf>) -> AResult<()>{
|
||||
let mut id = 0;
|
||||
//Construct allowed scripts
|
||||
let mut script_set = std::collections::HashSet::<String>::new();
|
||||
@ -639,10 +379,15 @@ fn replace() -> AResult<()>{
|
||||
if any_failed {
|
||||
println!("One or more scripts failed to replace.");
|
||||
}else{
|
||||
let mut dest=std::path::PathBuf::from("maps/unprocessed");
|
||||
dest.set_file_name(file_thing.file_name());
|
||||
let mut dest=PathBuf::from("maps/unprocessed");
|
||||
dest.push(file_thing.file_name());
|
||||
let output = std::io::BufWriter::new(std::fs::File::open(dest)?);
|
||||
rbx_binary::to_writer(output, &dom, &[dom.root_ref()])?;
|
||||
//write workspace:GetChildren()[1]
|
||||
let workspace_children=dom.root().children();
|
||||
if workspace_children.len()!=1{
|
||||
return Err(anyhow::Error::msg("there can only be one model"));
|
||||
}
|
||||
rbx_binary::to_writer(output, &dom, &[workspace_children[0]])?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@ -680,37 +425,38 @@ fn upload() -> AResult<()>{
|
||||
let mut input = std::io::BufReader::new(std::fs::File::open(file_thing.path())?);
|
||||
|
||||
let dom = get_dom(&mut input)?;
|
||||
let (modelname,creator,displayname) = get_mapinfo(&dom)?;
|
||||
let (modelname,creator,displayname,_) = get_mapinfo(&dom)?;
|
||||
|
||||
//Creator: [auto fill creator]
|
||||
//DisplayName: [auto fill DisplayName]
|
||||
//id: ["New" for blank because of my double enter key]
|
||||
print!("Model name: {}\nCreator: {}\nDisplayName: {}\nAction or Upload Asset Id: ",modelname,creator,displayname);
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
print!("Model name: {}\nCreator: {}\nDisplayName: {}\n",modelname,creator,displayname);
|
||||
let upload_action;
|
||||
loop{
|
||||
print!("Action or Upload Asset Id: ");
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut upload_action_string = String::new();
|
||||
std::io::stdin().read_line(&mut upload_action_string)?;
|
||||
if let Ok(parsed_upload_action)=upload_action_string.parse::<UploadAction>(){
|
||||
upload_action=parsed_upload_action;
|
||||
break;
|
||||
}else{
|
||||
print!("Action or Upload Asset Id: ");
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
}
|
||||
}
|
||||
match upload_action {
|
||||
UploadAction::Upload(asset_id) => {
|
||||
let status=std::process::Command::new("../rbxcompiler-linux-amd64")
|
||||
.arg("--compile=false")
|
||||
.arg("--group=6980477")
|
||||
.arg(format!("--asset={}",asset_id))
|
||||
.arg(format!("--input={}",file_thing.path().into_os_string().into_string().unwrap()))
|
||||
let status=std::process::Command::new("asset-tool")
|
||||
.args([
|
||||
"upload-asset",
|
||||
"--cookie-envvar","RBXCOOKIE",
|
||||
"--group-id","6980477"
|
||||
])
|
||||
.arg("--asset-id").arg(asset_id.to_string())
|
||||
.arg("--input-file").arg(file_thing.path().into_os_string().into_string().unwrap())
|
||||
.status()?;
|
||||
match status.code() {
|
||||
Some(0)=>{
|
||||
//move file
|
||||
let mut dest=std::path::PathBuf::from("maps/uploaded");
|
||||
let mut dest=PathBuf::from("maps/uploaded");
|
||||
dest.push(file_thing.file_name());
|
||||
std::fs::rename(file_thing.path(), dest)?;
|
||||
}
|
||||
@ -720,18 +466,21 @@ fn upload() -> AResult<()>{
|
||||
}
|
||||
UploadAction::Skip => continue,
|
||||
UploadAction::New => {
|
||||
let output=std::process::Command::new("../rbxcompiler-linux-amd64")
|
||||
.arg("--compile=false")
|
||||
.arg("--group=6980477")
|
||||
.arg("--new-asset=true")
|
||||
.arg(format!("--input={}",file_thing.path().into_os_string().into_string().unwrap()))
|
||||
let output=std::process::Command::new("asset-tool")
|
||||
.args([
|
||||
"create-asset",
|
||||
"--cookie-envvar","RBXCOOKIE",
|
||||
"--group-id","6980477"
|
||||
])
|
||||
.arg("--model-name").arg(modelname.as_str())
|
||||
.arg("--input-file").arg(file_thing.path().into_os_string().into_string().unwrap())
|
||||
.output()?;
|
||||
match output.status.code() {
|
||||
Some(0)=>{
|
||||
//print output
|
||||
println!("{}", std::str::from_utf8(output.stdout.as_slice())?);
|
||||
//move file
|
||||
let mut dest=std::path::PathBuf::from("maps/uploaded");
|
||||
let mut dest=PathBuf::from("maps/uploaded");
|
||||
dest.push(file_thing.file_name());
|
||||
std::fs::rename(file_thing.path(), dest)?;
|
||||
}
|
||||
@ -781,6 +530,27 @@ impl std::str::FromStr for ScriptActionParseResult {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_first_letter_lowercase(s:&str)->bool{
|
||||
s.chars().next().map(char::is_lowercase).unwrap_or(false)
|
||||
}
|
||||
|
||||
fn is_title_case(display_name:&str)->bool{
|
||||
display_name.len()!=0
|
||||
&&!is_first_letter_lowercase(display_name)
|
||||
&&{
|
||||
let display_name_pattern=lazy_regex::regex!(r"\b\S+");
|
||||
display_name_pattern.find_iter(display_name)
|
||||
.all(|capture|match capture.as_str(){
|
||||
"a"=>true,
|
||||
"an"=>true,
|
||||
"and"=>true,
|
||||
"the"=>true,
|
||||
"of"=>true,
|
||||
other=>!is_first_letter_lowercase(other),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn interactive() -> AResult<()>{
|
||||
let mut id=get_id()?;
|
||||
//Construct allowed scripts
|
||||
@ -788,21 +558,103 @@ fn interactive() -> AResult<()>{
|
||||
let mut allowed_map=get_allowed_map()?;
|
||||
let mut replace_map=get_replace_map()?;
|
||||
let mut blocked = get_blocked()?;
|
||||
let model_name_pattern=lazy_regex::regex!(r"^[a-z0-9_]+$");
|
||||
|
||||
'map_loop: for entry in std::fs::read_dir("maps/unprocessed")? {
|
||||
let file_thing=entry?;
|
||||
println!("processing map={:?}",file_thing.file_name());
|
||||
let mut input = std::io::BufReader::new(std::fs::File::open(file_thing.path())?);
|
||||
let mut dom = get_dom(&mut input)?;
|
||||
let (modelname,creator,displayname,displayname_ref)=get_mapinfo(&dom)?;
|
||||
|
||||
let script_refs = get_script_refs(&dom);
|
||||
|
||||
//check scribb
|
||||
let mut script_count=0;
|
||||
let mut replace_count=0;
|
||||
let mut block_count=0;
|
||||
|
||||
//if model name is illegal prompt for new name
|
||||
print!("Model name: {}\nCreator: {}\nDisplayName: {}\n",modelname,creator,displayname);
|
||||
if !model_name_pattern.is_match(modelname.as_str()){
|
||||
//illegal
|
||||
let new_model_name;
|
||||
loop{
|
||||
print!("Enter new model name: ");
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut input_string=String::new();
|
||||
std::io::stdin().read_line(&mut input_string)?;
|
||||
let input_final=match input_string.trim(){
|
||||
""=>modelname.as_str(),
|
||||
other=>other,
|
||||
};
|
||||
if model_name_pattern.is_match(input_final)
|
||||
||{
|
||||
//If you entered a new model name and it still doesn't like it, allow override
|
||||
println!("Final model name: {}",input_final);
|
||||
print!("Are you sure you want this model name? [y/N]:");
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut input_string=String::new();
|
||||
std::io::stdin().read_line(&mut input_string)?;
|
||||
match input_string.trim(){
|
||||
"y"=>true,
|
||||
_=>false,
|
||||
}
|
||||
}{
|
||||
new_model_name=input_final.to_owned();
|
||||
break;
|
||||
}
|
||||
}
|
||||
let model_instance=dom.get_by_ref_mut(dom.root().children()[0]).unwrap();
|
||||
model_instance.name=new_model_name;
|
||||
//mark file as edited so a new file is generated
|
||||
replace_count+=1;
|
||||
}
|
||||
if !is_title_case(displayname.as_str()){
|
||||
//illegal
|
||||
let new_display_name;
|
||||
loop{
|
||||
print!("Enter new display name: ");
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut input_string=String::new();
|
||||
std::io::stdin().read_line(&mut input_string)?;
|
||||
let input_final=match input_string.trim(){
|
||||
""=>displayname.as_str(),
|
||||
other=>other,
|
||||
};
|
||||
if is_title_case(input_string.trim())
|
||||
||{
|
||||
//If you entered a new display name and it still doesn't like it, allow override
|
||||
println!("Final display name: {}",input_final);
|
||||
print!("Are you sure you want this display name? [y/N]:");
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut input_string=String::new();
|
||||
std::io::stdin().read_line(&mut input_string)?;
|
||||
match input_string.trim(){
|
||||
"y"=>true,
|
||||
_=>false,
|
||||
}
|
||||
}{
|
||||
new_display_name=input_final.to_owned();
|
||||
break;
|
||||
}
|
||||
}
|
||||
let displayname_instance=dom.get_by_ref_mut(displayname_ref).unwrap();
|
||||
assert!(displayname_instance.properties.insert("Value".to_owned(),new_display_name.into()).is_some(),"StringValue we have a problem");
|
||||
//mark file as edited so a new file is generated
|
||||
replace_count+=1;
|
||||
}
|
||||
|
||||
let script_refs = get_script_refs(&dom)
|
||||
//grab the full path to the object in case it's deleted by another operation
|
||||
.into_iter()
|
||||
.filter_map(|referent|
|
||||
dom.get_by_ref(referent)
|
||||
.map(|script|
|
||||
(referent,get_full_name(&dom,script))
|
||||
)
|
||||
).collect::<Vec<_>>();
|
||||
|
||||
//check scribb
|
||||
let mut fail_type=Interactive::Passed;
|
||||
for &script_ref in script_refs.iter() {
|
||||
for (script_ref,script_full_name) in script_refs{
|
||||
if let Some(script)=dom.get_by_ref(script_ref){
|
||||
if let Some(rbx_dom_weak::types::Variant::String(source)) = script.properties.get("Source") {
|
||||
script_count+=1;
|
||||
@ -816,22 +668,20 @@ fn interactive() -> AResult<()>{
|
||||
ScriptAction::Replace(*replace_id)
|
||||
}else{
|
||||
//interactive logic goes here
|
||||
print!("unresolved source location={}\naction: ",get_full_name(&dom, script));
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
print!("unresolved source location={}\n",get_full_name(&dom, script));
|
||||
//load source into current.lua
|
||||
std::fs::write("current.lua",source)?;
|
||||
//prompt action in terminal
|
||||
//wait for input
|
||||
let script_action;
|
||||
loop{
|
||||
print!("action: ");
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut action_string = String::new();
|
||||
std::io::stdin().read_line(&mut action_string)?;
|
||||
if let Ok(parsed_script_action)=action_string.parse::<ScriptActionParseResult>(){
|
||||
script_action=parsed_script_action;
|
||||
break;
|
||||
}else{
|
||||
print!("action: ");
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
}
|
||||
}
|
||||
//update allowed/replace/blocked
|
||||
@ -911,18 +761,18 @@ fn interactive() -> AResult<()>{
|
||||
panic!("FATAL: failed to get source for {:?}",file_thing.file_name());
|
||||
}
|
||||
}else{
|
||||
panic!("FATAL: failed to get_by_ref {:?}",script_ref);
|
||||
println!("WARNING: script was deleted: {}",script_full_name);
|
||||
}
|
||||
}
|
||||
let mut dest=match fail_type{
|
||||
Interactive::Passed => {
|
||||
println!("map={:?} passed with {} {}",file_thing.file_name(),script_count,if script_count==1 {"script"}else{"scripts"});
|
||||
if replace_count==0{
|
||||
std::path::PathBuf::from("maps/passed")
|
||||
PathBuf::from("maps/passed")
|
||||
}else{
|
||||
//create new file
|
||||
println!("{} {} replaced - generating new file...",replace_count,if replace_count==1 {"script was"}else{"scripts were"});
|
||||
let mut dest=std::path::PathBuf::from("maps/passed");
|
||||
let mut dest=PathBuf::from("maps/passed");
|
||||
dest.push(file_thing.file_name());
|
||||
let output = std::io::BufWriter::new(std::fs::File::create(dest)?);
|
||||
//write workspace:GetChildren()[1]
|
||||
@ -932,16 +782,16 @@ fn interactive() -> AResult<()>{
|
||||
}
|
||||
rbx_binary::to_writer(output, &dom, &[workspace_children[0]])?;
|
||||
//move original to processed folder
|
||||
std::path::PathBuf::from("maps/unaltered")
|
||||
PathBuf::from("maps/unaltered")
|
||||
}
|
||||
},//write map into maps/processed
|
||||
Interactive::Blocked => {
|
||||
println!("map={:?} blocked with {}/{} {} blocked",file_thing.file_name(),block_count,script_count,if script_count==1 {"script"}else{"scripts"});
|
||||
std::path::PathBuf::from("maps/blocked")
|
||||
PathBuf::from("maps/blocked")
|
||||
},//write map into maps/blocked
|
||||
Interactive::Flagged => {
|
||||
println!("map={:?} flagged",file_thing.file_name());
|
||||
std::path::PathBuf::from("maps/flagged")
|
||||
PathBuf::from("maps/flagged")
|
||||
},//write map into maps/flagged
|
||||
};
|
||||
dest.push(file_thing.file_name());
|
||||
@ -950,91 +800,3 @@ fn interactive() -> AResult<()>{
|
||||
std::fs::write("id",id.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
fn unzip_all()->AResult<()>{
|
||||
for entry in std::fs::read_dir("maps/unprocessed")? {
|
||||
let file_thing=entry?;
|
||||
println!("processing map={:?}",file_thing.file_name());
|
||||
let mut input = std::io::BufReader::new(std::fs::File::open(file_thing.path())?);
|
||||
match maybe_gzip_decode(&mut input){
|
||||
Ok(ReaderType::GZip(mut readable)) => {
|
||||
//gzip
|
||||
let mut extracted:Vec<u8>=Vec::new();
|
||||
//read the entire thing to the end so that I can clone the data and write a png to processed images
|
||||
readable.read_to_end(&mut extracted)?;
|
||||
//write extracted
|
||||
let mut dest=std::path::PathBuf::from("maps/unzipped");
|
||||
dest.push(file_thing.file_name());
|
||||
std::fs::write(dest, &mut extracted)?;
|
||||
//delete ugly gzip file
|
||||
std::fs::remove_file(file_thing.path())?;
|
||||
},
|
||||
Ok(ReaderType::Raw(_)) => (),
|
||||
Err(e) => Err(e)?,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_attributes() -> AResult<()>{
|
||||
for entry in std::fs::read_dir("maps/unprocessed")? {
|
||||
let file_thing=entry?;
|
||||
println!("processing map={:?}",file_thing.file_name());
|
||||
let mut input = std::io::BufReader::new(std::fs::File::open(file_thing.path())?);
|
||||
let mut dom = get_dom(&mut input)?;
|
||||
|
||||
let button_refs = get_button_refs(&dom);
|
||||
|
||||
for &button_ref in &button_refs {
|
||||
if let Some(button)=dom.get_by_ref_mut(button_ref){
|
||||
match button.properties.get_mut("Attributes"){
|
||||
Some(rbx_dom_weak::types::Variant::Attributes(attributes))=>{
|
||||
println!("Appending Ref={} to existing attributes for {}",button_ref,button.name);
|
||||
attributes.insert("Ref".to_string(),rbx_dom_weak::types::Variant::String(button_ref.to_string()));
|
||||
},
|
||||
None=>{
|
||||
println!("Creating new attributes with Ref={} for {}",button_ref,button.name);
|
||||
let mut attributes=rbx_dom_weak::types::Attributes::new();
|
||||
attributes.insert("Ref".to_string(),rbx_dom_weak::types::Variant::String(button_ref.to_string()));
|
||||
button.properties.insert("Attributes".to_string(),rbx_dom_weak::types::Variant::Attributes(attributes));
|
||||
}
|
||||
_=>unreachable!("Fetching attributes did not return attributes."),
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut dest={
|
||||
let mut dest=std::path::PathBuf::from("maps/attributes");
|
||||
dest.push(file_thing.file_name());
|
||||
let output = std::io::BufWriter::new(std::fs::File::create(dest)?);
|
||||
//write workspace:GetChildren()[1]
|
||||
let workspace_children=dom.root().children();
|
||||
if workspace_children.len()!=1{
|
||||
return Err(anyhow::Error::msg("there can only be one model"));
|
||||
}
|
||||
rbx_binary::to_writer(output, &dom, &[workspace_children[0]])?;
|
||||
//move original to processed folder
|
||||
std::path::PathBuf::from("maps/unaltered")
|
||||
};
|
||||
dest.push(file_thing.file_name());
|
||||
std::fs::rename(file_thing.path(), dest)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> AResult<()> {
|
||||
let cli = Cli::parse();
|
||||
match cli.command {
|
||||
Commands::Download(map_list)=>download(map_list.maps),
|
||||
Commands::DownloadTextures(pathlist)=>download_textures(pathlist.paths),
|
||||
Commands::ConvertTextures=>convert_textures(),
|
||||
Commands::DownloadMeshes(pathlist)=>download_meshes(pathlist.paths),
|
||||
Commands::Extract(pathlist)=>extract(pathlist.paths),
|
||||
Commands::WriteAttributes=>write_attributes(),
|
||||
Commands::Interactive=>interactive(),
|
||||
Commands::Replace=>replace(),
|
||||
Commands::Scan=>scan(),
|
||||
Commands::UnzipAll=>unzip_all(),
|
||||
Commands::Upload=>upload(),
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user