Skip to content

Instantly share code, notes, and snippets.

@alyti
Last active June 18, 2019 09:36
Show Gist options
  • Save alyti/2bc16b74fd9d54b9bee126563194ba45 to your computer and use it in GitHub Desktop.
Save alyti/2bc16b74fd9d54b9bee126563194ba45 to your computer and use it in GitHub Desktop.
[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"
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