Last active
June 18, 2019 09:36
-
-
Save alyti/2bc16b74fd9d54b9bee126563194ba45 to your computer and use it in GitHub Desktop.
Windows binary here: https://cdn.discordapp.com/attachments/585363489346813965/590474832278323210/vroid-hair-merger.exe
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[package] | |
name = "vroid-hair-merger" | |
version = "0.1.0" | |
authors = ["Aleks <me@zeta.pm>"] | |
edition = "2018" | |
[dependencies] | |
serde = { version = "1.0", features = ["derive"] } | |
uuid = { version = "0.7", features = ["serde", "v4"] } | |
serde_json = "1.0" | |
clap = "2.33.0" | |
dirs = "2.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::path::PathBuf; | |
use std::io::BufReader; | |
use std::fs::{File, create_dir_all, copy}; | |
use std::time::SystemTime; | |
use std::collections::HashSet; | |
use dirs::config_dir; | |
use serde_json::{Value, json}; | |
use serde::{Deserialize, Serialize}; | |
use clap::{Arg, App}; | |
use uuid::Uuid; | |
fn main() { | |
let matches = App::new("VRoid Hair Preset Merger") | |
.version("1.0") | |
.author("Alti <alticodes@gmail.com>") | |
.about("Merges multiple hair presets for VRoid Studio by Pixiv.") | |
.arg(Arg::with_name("path") | |
.short("p") | |
.long("path") | |
.value_name("PATH") | |
.help("Sets a custom path to source presets from and export result to") | |
.takes_value(true)) | |
.arg(Arg::with_name("PRESET") | |
.help("Sets the input presets to use") | |
.required(true) | |
.index(1) | |
.multiple(true) | |
.min_values(2)) | |
.get_matches(); | |
let mut temp_root = config_dir().unwrap(); | |
temp_root.pop(); | |
temp_root.push("LocalLow"); | |
temp_root.push("pixiv"); | |
temp_root.push("VRoidStudio"); | |
temp_root.push("hair_presets"); | |
let presets_root = matches.value_of("path").unwrap_or(temp_root.to_str().unwrap()); | |
println!("Preset root: {}", presets_root); | |
let mut master_preset = Preset::default(); | |
let mut master_name = Vec::new(); | |
let mut materials_to_copy = Vec::new(); | |
let mut id_store = HashSet::new(); | |
let mut has_base_hair = false; | |
let new_preset_path = PathBuf::from(presets_root).join(format!("preset{}", SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs())); | |
let values = matches.values_of("PRESET").unwrap(); | |
for value in values { | |
let mut path = PathBuf::from(presets_root); | |
path.push(value); | |
if path.is_dir() { | |
println!("Valid path! {}", path.to_str().unwrap()) | |
} else { | |
path.pop(); | |
path.push(format!("preset{}", value)); | |
if !path.is_dir() { | |
panic!("Couldn't find `{}` preset.", value); | |
} | |
} | |
let preset_json_file = path.join("preset.json"); | |
if !preset_json_file.is_file() { | |
panic!("Couldn't find preset.json for `{}` preset.", value) | |
} | |
let f = File::open(preset_json_file.to_str().unwrap()).unwrap(); | |
let reader = BufReader::new(f); | |
let mut p: Preset = serde_json::from_reader(reader).unwrap(); | |
for material in p.material_set.items.clone() { | |
let id = material["_MainTextureId"].as_str(); | |
if id.is_none() { | |
continue; | |
} | |
let mut asset_path = path.join("materials"); | |
asset_path.push("rendered_textures"); | |
asset_path.push(id.unwrap()); | |
asset_path.set_extension("png"); | |
materials_to_copy.push(asset_path); | |
} | |
if master_preset.material_set.id == "" { | |
println!("Loaded preset `{}` as master, it contains roughly {} hairishes and {} materials.", p.display_name, p.hairishes.len(), p.material_set.items.len()); | |
master_preset.material_set.clone_from(&p.material_set); | |
} else { | |
println!("Loaded preset `{}`, it contains roughly {} hairishes and {} materials.", p.display_name, p.hairishes.len(), p.material_set.items.len()); | |
master_preset.material_set.items.append(&mut p.material_set.items); | |
} | |
if master_preset.metadata.is_none() { | |
master_preset.metadata.clone_from(&p.metadata); | |
} | |
if master_preset.hairbones.is_none() { | |
master_preset.hairbones.clone_from(&p.hairbones); | |
} | |
for mut hair in p.hairishes { | |
let id = hair["Id"].as_str().unwrap().to_string(); | |
if !id_store.insert(id) { | |
hair["Id"] = json!(Uuid::new_v4()); | |
} | |
if hair["Type"].as_i64().unwrap() == 3 { | |
if has_base_hair { | |
println!("Found additional base hairs in `{}` preset, but master preset already has one, will skip this one.", p.display_name); | |
continue; | |
} else { | |
has_base_hair = true; | |
} | |
} | |
if !hair["Visibility"].as_bool().unwrap() { | |
continue; | |
} | |
hair["DisplayName"] = json!(format!("{} - {}", p.display_name, hair["DisplayName"].as_str().unwrap())); | |
master_preset.hairishes.push(hair); | |
} | |
master_name.push(p.display_name); | |
} | |
master_preset.display_name = format!("Merged [{}]", master_name.join(", ")); | |
println!("Merged preset `{}`, it contains roughly {} hairishes and {} materials.", master_preset.display_name, master_preset.hairishes.len(), master_preset.material_set.items.len()); | |
let mut materials_path = new_preset_path.join("materials"); | |
materials_path.push("rendered_textures"); | |
assert!(create_dir_all(materials_path.to_str().unwrap()).is_ok()); | |
for material in materials_to_copy { | |
assert!(copy(material.to_str().unwrap(), materials_path.join(material.file_name().unwrap()).to_str().unwrap()).is_ok()); | |
} | |
let file = File::create(new_preset_path.join("preset.json").to_str().unwrap()).unwrap(); | |
let result = serde_json::to_writer(file, &master_preset); | |
println!("Result from exporting: {:?}", result); | |
} | |
#[derive(Serialize, Deserialize, Default, Clone, Debug)] | |
struct Materials { | |
#[serde(rename = "_Materials")] | |
items: Vec<Value>, | |
#[serde(rename = "_MaterialSetVersion")] | |
version: Value, | |
#[serde(rename = "_Id")] | |
id: String, | |
#[serde(rename = "_SphereAddTextureIdPrefix")] | |
prefix: String | |
} | |
#[derive(Serialize, Deserialize, Default, Clone, Debug)] | |
struct Bones { | |
#[serde(rename = "Groups")] | |
groups: Vec<Value>, | |
#[serde(rename = "RootBoneName")] | |
root_bone_name: String, | |
} | |
#[derive(Serialize, Deserialize, Default, Debug)] | |
struct Preset { | |
#[serde(rename = "Hairishes")] | |
hairishes: Vec<Value>, | |
#[serde(rename = "_MaterialSet")] | |
material_set: Materials, | |
#[serde(rename = "_DisplayName")] | |
display_name: String, | |
#[serde(rename = "_MetaData")] | |
metadata: Option<Value>, | |
#[serde(rename = "_HairBoneStore")] | |
hairbones: Option<Bones>, | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment