Compare commits

...

26 Commits

Author SHA1 Message Date
c415ffbdab default to current name and confirm override 2024-08-24 19:40:09 -07:00
cbc818bd03 remove a panic 2024-08-13 16:20:32 -07:00
53d2f7a5e8 v1.1.1 fix displayname 2024-08-12 16:23:42 -07:00
96d1cc87a2 update deps 2024-08-12 16:23:42 -07:00
5915dd730f redo displayname test 2024-08-12 16:23:42 -07:00
e626131d95 v1.1.0 asset tool upgrade (TODO: directly use rbx_asset lib) 2024-07-15 20:24:28 -07:00
69ffbf4837 update deps 2024-07-15 20:24:28 -07:00
167be8f587 update asset tool to 0.4.x 2024-07-15 20:22:54 -07:00
e92528ad83 reject uncapitalized display names 2024-06-03 08:47:16 -07:00
8e9c76d6f8 use strafesnet deps 2024-05-30 02:02:46 -07:00
a5c48d4684 v1.0.1 use asset tool + prompt for model name 2024-04-25 19:02:11 -07:00
1b5eec9eaf if model name is illegal prompt for new name 2024-04-25 04:42:31 -07:00
ef5703f282 condense prompt logic 2024-04-25 04:34:31 -07:00
9685301b30 use asset tool for upload 2024-04-25 01:10:15 -07:00
d2b455c87b rename package and de-version to pre-map-tool functionality 2024-03-17 10:29:24 -07:00
9de2790cc8 remove map-tool deps 2024-03-17 10:29:24 -07:00
47e93325ad remove map-tool + asset-tool functions 2024-03-08 10:15:00 -08:00
de9712b7a1 clarify function name 2024-03-08 10:10:26 -08:00
c2d0a4487c misc edits 2024-03-08 10:01:54 -08:00
dc9fd2c442 import PathBuf 2024-03-08 09:55:17 -08:00
4199d41d3f timeless License 2024-01-30 18:38:47 -08:00
7fbcb206ff probably was wrong but idc about testing it 2024-01-30 16:39:57 -08:00
a17901d473 v1.4.0 valve maps 2024-01-12 11:34:09 -08:00
b88c6b899a commands for valve maps 2024-01-12 11:32:12 -08:00
835d4bbecd add valve map deps 2024-01-12 11:32:12 -08:00
b756dc979c move main to top 2024-01-12 11:32:12 -08:00
5 changed files with 385 additions and 1377 deletions

2
.cargo/config.toml Normal file
View File

@ -0,0 +1,2 @@
[registries.strafesnet]
index = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/"

1126
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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
View File

@ -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.

View File

@ -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(),
}
}