diff --git a/apps/cli/p9_scene_tool/Cargo.toml b/apps/cli/p9_scene_tool/Cargo.toml deleted file mode 100644 index d65a911..0000000 --- a/apps/cli/p9_scene_tool/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "p9_scene_tool" -version.workspace = true -authors.workspace = true -edition.workspace = true - -[dependencies] -build-time = "0.1.3" -clap = { workspace = true } -const_format = "0.2.32" -grim = { workspace = true, features = [ "midi" ] } -log = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -simplelog = { workspace = true } -thiserror = { workspace = true } - -[dev-dependencies] -rstest = { workspace = true } - -[lints] -workspace = true \ No newline at end of file diff --git a/apps/cli/p9_scene_tool/src/apps/milo2midi.rs b/apps/cli/p9_scene_tool/src/apps/milo2midi.rs deleted file mode 100644 index 5f26ed3..0000000 --- a/apps/cli/p9_scene_tool/src/apps/milo2midi.rs +++ /dev/null @@ -1,232 +0,0 @@ -use crate::apps::{SubApp}; -use clap::Parser; -use grim::dta::DataArray; -use std::error::Error; -use std::fmt::Display; -use std::fs; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use thiserror::Error; - -use grim::{Platform, SystemInfo}; -use grim::io::*; -use grim::midi::{MidiEvent, MidiTextType, MidiFile, MidiText, MidiTrack}; -use grim::scene::{Object, ObjectDir, ObjectDirBase, PackedObject, PropAnim, PropKeysEvents, Tex, AnimRate}; -use grim::texture::{Bitmap, write_rgba_to_file}; - -#[derive(Parser, Debug)] -pub struct Milo2MidiApp { - #[arg(help = "Path to input milo scene", required = true)] - pub milo_path: String, - #[arg(help = "Path to output MIDI file", required = true)] - pub midi_path: String, - #[arg(short = 'm', long, help = "Base MIDI file")] - pub base_midi: Option -} - -impl SubApp for Milo2MidiApp { - fn process(&mut self) -> Result<(), Box> { - let milo_path = PathBuf::from(&self.milo_path); - let output_midi_path = PathBuf::from(&self.midi_path); - - let mut mid = self.base_midi - .as_ref() - .and_then(|path| MidiFile::from_path(path)) - .unwrap_or_default(); - - // TODO: Remove debug output - for track in mid.tracks.iter() { - let track_name = track.name - .as_ref() - .map(|n| n.as_str()) - .unwrap_or("???"); - - let ev_count = track.events.len(); - - println!("\"{track_name}\" : {ev_count} events"); - } - - // Open milo - let mut stream = FileStream::from_path_as_read_open(&milo_path)?; - let milo = MiloArchive::from_stream(&mut stream)?; - - // Unpack dir and entries - let system_info = SystemInfo::guess_system_info(&milo, &milo_path); - let mut obj_dir = milo.unpack_directory(&system_info)?; - obj_dir.unpack_entries(&system_info)?; - - for entry in obj_dir.get_entries() { - let name = entry.get_name(); - let obj_type = entry.get_type(); - - let is_packed = entry.is_packed(); - - println!("{name} | {obj_type} (packed: {is_packed})"); - - if let Object::PropAnim(prop_anim) = entry { - let extra_tracks = process_prop_anim(prop_anim, &mid); - mid.add_tracks_with_realtime_positions(extra_tracks, false); - } - } - - // Save output midi file - let midi_dir = output_midi_path.parent().unwrap(); - if !midi_dir.exists() { - // Not found, create directory - fs::create_dir_all(&midi_dir)?; - } - - mid.write_to_file(output_midi_path); - - Ok(()) - } -} - -fn process_prop_anim(prop_anim: &PropAnim, _base_mid: &MidiFile) -> Vec { - // TODO: Pre-parse tempo track for faster realtime to tick pos calculation - - const GDRB_CHARACTERS: [(&str, &str); 3] = [ - ("BILLIE", "BILLIEJOE"), - ("MIKE", "MIKEDIRNT"), - ("TRE", "TRECOOL"), - ]; - - // Create tracks - let mut mapped_tracks = GDRB_CHARACTERS - .iter() - .map(|(c_short, c_long)| (format!("_{}", c_long.to_lowercase()), MidiTrack { - name: Some(c_short.to_string()), - events: Vec::new() - })) - .collect::>(); - - let mut venue_track = MidiTrack { - name: Some(String::from("VENUE GDRB")), - events: Vec::new() - }; - - let fps = match prop_anim.rate { - AnimRate::k30_fps | AnimRate::k30_fps_ui | AnimRate::k30_fps_tutorial => 30., - _ => panic!("Unsupported anim rate of {:?}", prop_anim.rate) - }; - - let track_keys = mapped_tracks.keys().map(|k| k.to_string()).collect::>(); - - for prop_keys in prop_anim.keys.iter() { - let _target = prop_keys.target.as_str(); // Don't care for now - - // Assume single symbol for now (most common for TBRB/GDRB song anims) - let property = prop_keys - .property - .first() - .and_then(|node| match node { - DataArray::Symbol(s) => s.as_utf8(), - _ => None, - }); - - if property.is_none() { - continue; - } - - let mut property = (unsafe { property.unwrap_unchecked() }).to_string(); - let mut track = &mut venue_track; // Use venue track by default - - for track_key in track_keys.iter() { - if property.contains(track_key) { - // Update property name and use dedicated character track - property = property.replace(track_key, ""); - track = unsafe { mapped_tracks.get_mut(track_key).unwrap_unchecked() }; - - break; - } - } - - // Map events to display vecs - let events_as_display: Vec<(f32, Vec<&dyn Display>)> = match &prop_keys.events { - PropKeysEvents::Float(events) => events - .iter() - .map(|ev| (ev.pos, vec![ - &ev.value as &dyn Display - ])) - .collect(), - PropKeysEvents::Color(events) => events - .iter() - .map(|ev| (ev.pos, vec![ - &ev.value.r as &dyn Display, - &ev.value.g as &dyn Display, - &ev.value.b as &dyn Display, - &ev.value.a as &dyn Display - ])) - .collect(), - PropKeysEvents::Object(events) => events - .iter() - .map(|ev| (ev.pos, vec![ - &ev.text1 as &dyn Display, - &ev.text2 as &dyn Display - ])) - .collect(), - PropKeysEvents::Bool(events) => events - .iter() - .map(|ev| (ev.pos, vec![ - if ev.value { &"TRUE" } else { &"FALSE" } as &dyn Display - ])) - .collect(), - PropKeysEvents::Quat(events) => events - .iter() - .map(|ev| (ev.pos, vec![ - &ev.value.x as &dyn Display, - &ev.value.y as &dyn Display, - &ev.value.z as &dyn Display, - &ev.value.w as &dyn Display - ])) - .collect(), - PropKeysEvents::Vector3(events) => events - .iter() - .map(|ev| (ev.pos, vec![ - &ev.value.x as &dyn Display, - &ev.value.y as &dyn Display, - &ev.value.z as &dyn Display - ])) - .collect(), - PropKeysEvents::Symbol(events) => events - .iter() - .map(|ev| (ev.pos, vec![ - &ev.text as &dyn Display - ])) - .collect() - }; - - for (pos, values) in events_as_display { - let realtime_pos = (pos as f64 / fps) * 1000.; // Convert from frame pos to realtime (ms) - - // Joins values into single string - // TODO: Look at making this more efficient - let values_formatted = values - .iter() - .map(|v| v.to_string()) - .filter(|v| !v.is_empty()) - .collect::>() - .join(" "); - - let text = format!("[{property} ({values_formatted})]"); - - // Add event - track.events.push(MidiEvent::Meta(MidiText { - pos: 0, // Calculated elsewhere - pos_realtime: Some(realtime_pos), - text: MidiTextType::Event(text.into_bytes().into_boxed_slice()) - })) - } - } - - let mut new_tracks = Vec::new(); - for (_, char_long) in GDRB_CHARACTERS.iter() { - let key = format!("_{}", char_long.to_lowercase()); - - let track = mapped_tracks.remove(key.as_str()).unwrap(); - new_tracks.push(track); - } - - new_tracks.push(venue_track); - new_tracks -} \ No newline at end of file diff --git a/apps/cli/p9_scene_tool/src/apps/mod.rs b/apps/cli/p9_scene_tool/src/apps/mod.rs deleted file mode 100644 index 79b23ac..0000000 --- a/apps/cli/p9_scene_tool/src/apps/mod.rs +++ /dev/null @@ -1,61 +0,0 @@ -use clap::{Parser, Subcommand}; -use std::error::Error; - -mod milo2midi; -mod newproject; -mod project2milo; -use self::milo2midi::*; -use self::newproject::*; -use self::project2milo::*; - -// From Cargo.toml -pub const PKG_NAME: &'static str = env!("CARGO_PKG_NAME"); -// pub const VERSION: &'static str = env!("CARGO_PKG_VERSION"); -pub const VERSION: &'static str = get_prerelease_version(); - -const fn get_prerelease_version() -> &'static str { - const BUILD_DATE: &str = build_time::build_time_local!("%Y%m%d"); - const_format::formatcp!("prealpha-{}", BUILD_DATE) -} - -pub(crate) trait SubApp { - fn process(&mut self) -> Result<(), Box>; -} - -#[derive(Parser, Debug)] -#[command(name = PKG_NAME, version = VERSION, about = "Use this tool for modding scenes from milo engine based games (project 9)")] -struct Options { - #[command(subcommand)] - commands: SubCommand, -} - -#[derive(Subcommand, Debug)] -enum SubCommand { - #[command(name = "milo2midi", about = "Creates MIDI from milo scene")] - Milo2Midi(Milo2MidiApp), - #[command(name = "newproj", about = "Create new song project from scratch")] - NewProject(NewProjectApp), - #[command(name = "proj2milo", about = "Build song milo archive(s) from input project")] - Project2Milo(Project2MiloApp) -} - -#[derive(Debug)] -pub struct P9SceneTool { - options: Options, -} - -impl P9SceneTool { - pub fn new() -> P9SceneTool { - P9SceneTool { - options: Options::parse() - } - } - - pub fn run(&mut self) -> Result<(), Box> { - match &mut self.options.commands { - SubCommand::Milo2Midi(app) => app.process(), - SubCommand::NewProject(app) => app.process(), - SubCommand::Project2Milo(app) => app.process() - } - } -} \ No newline at end of file diff --git a/apps/cli/p9_scene_tool/src/apps/newproject.rs b/apps/cli/p9_scene_tool/src/apps/newproject.rs deleted file mode 100644 index 0180210..0000000 --- a/apps/cli/p9_scene_tool/src/apps/newproject.rs +++ /dev/null @@ -1,162 +0,0 @@ -use crate::apps::{SubApp}; -use crate::models::*; -use clap::Parser; -use grim::midi::{MidiFile, MidiTrack}; -use std::error::Error; -use std::fs::{copy, create_dir_all, read, remove_dir_all, write}; -use std::path::{Path, PathBuf}; -use thiserror::Error; - -#[derive(Parser, Debug)] -pub struct NewProjectApp { - #[arg(help = "Path to output project directory", required = true)] - pub output_path: String, - #[arg(short, long, help = "Shortname of song (ex. \"temporarysec\")", required = true)] - pub name: String, - #[arg(short, long, help = "Use GDRB format", required = false)] - pub gdrb: bool -} - -impl SubApp for NewProjectApp { - fn process(&mut self) -> Result<(), Box> { - let ouput_dir = PathBuf::from(&self.output_path); - if !ouput_dir.exists() { - // Create dir - create_dir_all(&ouput_dir).unwrap(); - } - - // Create extra folder - let extra_dir = ouput_dir.join("extra"); - if !extra_dir.exists() { - create_dir_all(&extra_dir).unwrap(); - //write(extra_dir.join("EXTRA_MILO_RELATED_FILES_HERE"), "").unwrap(); - println!("Created extras directory"); - } - - // Create lipsync folder - let lipsync_dir = ouput_dir.join("lipsync"); - if !lipsync_dir.exists() { - create_dir_all(&lipsync_dir).unwrap(); - //write(lipsync_dir.join("LIPSYNC_HERE"), "").unwrap(); - println!("Created lipsync directory"); - } - - // Write midi file - let midi_path = ouput_dir.join("venue.mid"); - create_default_mid(&midi_path, self.gdrb)?; - - // Write json file - let song = create_p9_song(&self.name, self.gdrb); - //let song_json = serde_json::to_string_pretty(&song)?; - let song_json = crate::formatter::to_string(&song)?; - let song_json_path = ouput_dir.join("song.json"); - - write(song_json_path, song_json).unwrap(); - println!("Wrote \"song.json\""); - - let output_dir_str = ouput_dir.as_path().to_str().unwrap_or("???"); // Ugh why so hacky? - let game_format = if self.gdrb { "GDRB" } else { "TBRB" }; - - println!("Successfully created project for {game_format} in \"{output_dir_str}\""); - Ok(()) - } -} - -fn create_p9_song(name: &str, gdrb: bool) -> P9Song { - let preferences = if gdrb { - SongPreferences::GDRB(GDRBSongPreferences { - venue: String::from("dookie"), - mike_instruments: vec![ - String::from("bass_g3") - ], - billie_instruments: vec![ - String::from("guitar_blue01") - ], - tre_instruments: vec![ - String::from("drum_dw") - ], - tempo: String::from("medium"), - song_clips: String::from("none"), - normal_outfit: String::from("dookie"), - bonus_outfit: String::from("dookie"), - drum_set: String::from("drum_dw"), - era: String::from("early"), - cam_directory: String::from(""), - media_directory: String::from(""), - song_intro_cam: String::from(""), - win_cam: String::from(""), - }) - } else { - SongPreferences::TBRB(TBRBSongPreferences { - venue: String::from("dreamscape"), - mini_venues: vec![ - String::from("abbeyroad01default") - ], - scenes: Vec::new(), - dreamscape_outfit: String::from("sixtyeight"), - studio_outfit: String::from("sixtyeight_hdp"), - george_instruments: vec![ - String::from("guitar_rh_gibson_lespaul_red") - ], - john_instruments: vec![ - String::from("guitar_rh_epi65casino_strip") - ], - paul_instruments: vec![ - String::from("bass_lh_ricken_4001s_stripped") - ], - ringo_instruments: vec![ - String::from("drum_dream04") - ], - tempo: String::from("medium"), - song_clips: String::from("none"), - dreamscape_font: String::from("none"), - george_amp: String::from("none"), - john_amp: String::from("none"), - paul_amp: String::from("none"), - mixer: String::from("none"), - dreamscape_camera: String::from("kP9DreamSlow"), - lyric_part: String::from("PART HARM1") - }) - }; - - P9Song { - name: name.to_owned(), - preferences, - ..P9Song::default() - } -} - -fn create_default_mid(mid_path: &Path, gdrb: bool) -> Result<(), std::io::Error> { - const TBRB_TRACK_NAMES: [&str; 5] = [ - "PAUL", - "JOHN", - "GEORGE", - "RINGO", - "VENUE" - ]; - - const GDRB_TRACK_NAMES: [&str; 4] = [ - "BILLIE", - "MIKE", - "TRE", - "VENUE GDRB" - ]; - - let mut midi = MidiFile::default(); - - // Create basic tempo track - // Nothing to do? - - // Add other tracks - let track_names = if gdrb { GDRB_TRACK_NAMES.as_slice() } else { TBRB_TRACK_NAMES.as_slice() }; - for track_name in track_names { - midi.tracks.push(MidiTrack { - name: Some(track_name.to_string()), - events: Vec::new() - }); - } - - midi.write_to_file(mid_path); - println!("Wrote \"venue.mid\""); - Ok(()) -} \ No newline at end of file diff --git a/apps/cli/p9_scene_tool/src/apps/project2milo.rs b/apps/cli/p9_scene_tool/src/apps/project2milo.rs deleted file mode 100644 index ba80c1e..0000000 --- a/apps/cli/p9_scene_tool/src/apps/project2milo.rs +++ /dev/null @@ -1,695 +0,0 @@ -use crate::apps::SubApp; -use crate::helpers::*; -use crate::models::*; -use clap::Parser; -use grim::Platform; -use grim::SystemInfo; -use grim::dta::DataArray; -use grim::dta::DataString; -use grim::io::*; -use grim::midi::{MidiFile, MidiTrack, MidiEvent, MidiTextType, MidiText}; -use grim::scene::*; -use log::{debug, error, info, warn}; -use serde::Deserialize; -use serde_json::Deserializer; -use std::collections::HashMap; -use std::error::Error; -use std::fs::OpenOptions; -use std::fs::{copy, create_dir_all, File, read, remove_dir_all, write}; -use std::io::Read; -use std::io::Write; -use std::path::{Path, PathBuf}; -use thiserror::Error; - -// TODO: Rename to something like 'compile' or 'build' -#[derive(Parser, Debug)] -pub struct Project2MiloApp { - #[arg(help = "Path to input project directory", required = true)] - pub input_path: String, - #[arg(help = "Path to build output", required = true)] - pub output_path: String, - #[arg(short, long, help = "Enable to leave output milo archive(s) uncompressed", required = false)] - pub uncompressed: bool, - #[arg(short, long, help = "Platform (ps3, wii, x360)", required = false, default_value = "x360")] - pub platform: String -} - -impl SubApp for Project2MiloApp { - fn process(&mut self) -> Result<(), Box> { - let input_dir = PathBuf::from(&self.input_path); - if !input_dir.exists() { - // TODO: Throw proper error - error!("Input directory {:?} doesn't exist", input_dir); - return Ok(()) - } - - // Open song file - let song_json_path = input_dir.join("song.json"); - let song_json = read(song_json_path)?; - let song = serde_json::from_slice::(song_json.as_slice())?; - - let game_abbr = if song.preferences.is_gdrb() { "GDRB" } else { "TBRB" }; - info!("Loading song project for {game_abbr}..."); - - //dbg!(&song); - - // Get lipsync file(s) - let mut lipsyncs = get_lipsync(&input_dir.join("lipsync").as_path(), song.preferences.is_gdrb()); - - // Load venue midi - let mut prop_anim = load_midi(&input_dir, song.preferences.is_gdrb()); - - // Create song prefs file - let song_pref = create_song_pref(&song); - - // TODO: Is "LightPreset.pst" needed? - - // Create milos files... - - let output_dir = PathBuf::from(&self.output_path); - if !output_dir.exists() { - // Create outout path if it doesn't exist - create_dir_all(&output_dir).expect("Failed to create output directory"); - } - - // Get platform ext - let (platform, platform_ext) = match self.platform.to_ascii_lowercase().as_str() { - "ps3" => (Platform::PS3, "ps3"), - "wii" => (Platform::Wii, "wii"), - _ => (Platform::X360, "xbox") - }; - - let sys_info = SystemInfo { - version: 25, - platform: platform, - endian: IOEndian::Big, - }; - - // Name, object dir init - let object_dirs = [ - (format!("{}_ap.milo_{platform_ext}", &song.name), { - let mut obj_dir = create_object_dir_for_song(&song.name, &sys_info); - - let entries = obj_dir.get_entries_mut(); - entries.push(song_pref); - - if let Some(prop_anim) = prop_anim.take() { - entries.push(prop_anim); - } - - obj_dir - }), - (format!("{}.milo_{platform_ext}", &song.name), { - let mut obj_dir = create_object_dir_for_lipsync(&sys_info); - - // Add lipsync files - obj_dir.get_entries_mut().append(&mut lipsyncs); - - obj_dir - }), - ]; - - for (file_name, object_dir) in object_dirs { - let block_type = match self.uncompressed { - true => BlockType::TypeA, - _ => BlockType::TypeB - }; - - let archive = MiloArchive::from_object_dir(&object_dir, &sys_info, Some(block_type))?; - - // Write to file - let milo_path = output_dir.join(&file_name); - let mut stream = FileStream::from_path_as_write_create(&milo_path)?; - archive.write_to_stream(&mut stream)?; - - info!("Wrote \"{file_name}\"") - } - - Ok(()) - } -} - -fn create_song_pref(song: &P9Song) -> Object { - let song_pref = match &song.preferences { - SongPreferences::GDRB(prefs) => P9SongPref { - name: String::from("P9SongPref"), - note: format!("Generated by {} ({})", super::PKG_NAME, super::VERSION), - venue: prefs.venue.to_string(), - instruments: [ - prefs.mike_instruments.to_owned(), - prefs.billie_instruments.to_owned(), - Vec::new(), - prefs.tre_instruments.to_owned() - ], - tempo: prefs.tempo.to_owned(), - song_clips: prefs.song_clips.to_owned(), - normal_outfit: prefs.normal_outfit.to_string(), - bonus_outfit: prefs.bonus_outfit.to_string(), - drum_set: prefs.drum_set.to_owned(), - era: prefs.era.to_owned(), - song_intro_cam: prefs.song_intro_cam.to_owned(), - win_cam: prefs.win_cam.to_owned(), - ..Default::default() - }, - _ => todo!() - }; - - Object::P9SongPref(song_pref) -} - -fn get_lipsync(lipsync_dir: &Path, is_gdrb: bool) -> Vec { - const GDRB_LIPSYNC_NAMES: [&str; 4] = [ - "song.lipsync", - "billiejoe.lipsync", - "mikedirnt.lipsync", - "trecool.lipsync" - ]; - - const TBRB_LIPSYNC_NAMES: [&str; 4] = [ - "george.lipsync", - "john.lipsync", - "paul.lipsync", - "ringo.lipsync" - ]; - - let lipsyncs = lipsync_dir - .find_files_with_depth(FileSearchDepth::Immediate) - .unwrap_or_default() - .into_iter() - .filter(|lip| lip - .file_name() - .and_then(|f| f.to_str()) - .map(|p| p.ends_with(".lipsync")) - .unwrap_or_default()) - .collect::>(); - - if lipsyncs.is_empty() { - warn!("No lipsync files found in {:?}", lipsync_dir); - return Vec::new(); - } - - // Validate lipsync file names - let lipsync_names = if is_gdrb { &GDRB_LIPSYNC_NAMES } else { &TBRB_LIPSYNC_NAMES }; - - for lipsync_file in lipsyncs.iter() { - let file_name = lipsync_file.file_name().and_then(|f| f.to_str()).unwrap(); - - info!("Found \"{}\"", &file_name); - - let mut is_valid = false; - - for name in lipsync_names.iter() { - if file_name.eq(*name) { - is_valid = true; - break; - } - } - - if !is_valid { - warn!("Lipsync with file name \"{file_name}\" is invalid. Expected: {lipsync_names:?}"); - } - } - - // Get byte data for lipsync files - lipsyncs - .iter() - .map(|lip_path| { - let mut buffer = Vec::new(); - - let mut file = File::open(lip_path).expect(format!("Can't open {:?}", lip_path).as_str()); - file.read_to_end(&mut buffer).expect(format!("Can't read data from {:?}", lip_path).as_str()); - - let file_name = lip_path.file_name().and_then(|f| f.to_str()).unwrap(); - - Object::Packed(PackedObject { - name: file_name.to_string(), - object_type: String::from("CharLipSync"), - data: buffer, - }) - }) - .collect() -} - -fn load_midi(project_dir: &Path, is_gdrb: bool) -> Option { - const GDRB_CHARACTERS: [(&str, &str); 3] = [ - ("BILLIE", "billiejoe"), - ("MIKE", "mikedirnt"), - ("TRE", "trecool"), - ]; - - const TBRB_CHARACTERS: [(&str, &str); 4] = [ - ("GEORGE", "george"), - ("JOHN", "john"), - ("PAUL", "paul"), - ("RINGO", "ringo"), - ]; - - // Open midi - let mid_path = project_dir.join("venue.mid"); // TODO: Check if midi exists first? - if !mid_path.exists() { - // TODO: Throw proper error. Not sure if should halt process though... - error!("Can't find \"venue.mid\""); - return None; - } - - let mid = MidiFile::from_path(mid_path).unwrap(); - let mut prop_keys = Vec::new(); - - // Parse venue track - let venue_track_name = if is_gdrb { "VENUE GDRB" } else { "VENUE" }; - let venue_track = mid.get_track_with_name(venue_track_name); - if let Some(track) = venue_track { - let mut keys = load_venue_track(track, is_gdrb); - prop_keys.append(&mut keys); - } else { - warn!("Track \"{venue_track_name}\" not found in midi"); - } - - // Parse each character - let mut char_loaded = false; - let char_track_names = if is_gdrb { GDRB_CHARACTERS.as_slice() } else { TBRB_CHARACTERS.as_slice() }; - for (char_track_name, long_name) in char_track_names.iter() { - let char_track = mid.get_track_with_name(char_track_name); - - if let Some(track) = char_track { - let mut keys = load_char_track(track, long_name, is_gdrb); - prop_keys.append(&mut keys); - - char_loaded = true; - } - } - - if !char_loaded { - let char_track_names = char_track_names.iter().map(|(n, _)| n).collect::>(); - warn!("No character anim tracks found in midi. Expected: {char_track_names:?}"); - } - - Some(Object::PropAnim(PropAnim { - name: String::from("song.anim"), - type2: String::from("song_anim"), - note: format!("Generated by {} ({})", super::PKG_NAME, super::VERSION), - keys: prop_keys, - ..Default::default() - })) -} - -fn load_track(track: &MidiTrack, properties: &[(&str, u32, Option<&str>, u32, fn() -> PropKeysEvents)]) -> Vec { - let mut prop_keys = HashMap::new(); // property -> keys - let track_name = track.name.as_ref().map(|n| n.as_str()).unwrap_or("???"); - - for ev in track.events.iter() { - let (_pos, _pos_realtime, text) = match ev { - MidiEvent::Meta(MidiText { pos, pos_realtime, text: MidiTextType::Event(text), .. }) => (*pos, pos_realtime.unwrap(), std::str::from_utf8(text).ok()), - _ => continue, - }; - - let text = if let Some(txt) = text { - txt - } else { - // TODO: Output warning and midi timestamp/realtime pos - continue; - }; - - let parsed_text = if let Some(parsed) = FormattedAnimEvent::try_from_str(text) { parsed } else { continue; }; - let property = parsed_text.get_property(); - - if !prop_keys.contains_key(property) { - // Validate property - match properties.iter().find(|(p, ..)| p.eq(&parsed_text.get_property())) { - Some((property, interpolation, interp_handler, unk_enum, init_events)) => { - // Create and insert new prop key - prop_keys.insert(*property, PropKeys { - target: String::from("P9Director"), // Note: Implicitly P9Director - property: vec![ - DataArray::Symbol(DataString::from_string(property.to_string())) - ], - interpolation: *interpolation, - interp_handler: interp_handler - .map(|h| h.to_string()) - .unwrap_or_default(), - unknown_enum: *unk_enum, - events: init_events() - }); - }, - _ => { - // Property not supported - // TODO: Show time in log - warn!("Event for property \"{property}\" is not supported"); - continue; - } - }; - } - - let key = prop_keys.get_mut(property).unwrap(); - let pos = ((ev.get_pos_realtime().unwrap() * 30.) / 1000.) as f32; // TODO: Probably make fps a variable - - match &mut key.events { - PropKeysEvents::Float(evs) => { - let anim_ev = AnimEventFloat { - pos, - value: match parsed_text.try_parse_values::<1, f32>() { - [Some(f)] => f, - _ => { - // TODO: Show position - warn!("Unable to parse \"{}\"", parsed_text.get_text()); - continue; - } - } - }; - - evs.push(anim_ev); - }, - PropKeysEvents::Color(evs) => { - let color = match parsed_text.try_parse_values::<4, f32>() { - [Some(r), Some(g), Some(b), Some(a)] => Color4 { r, g, b, a }, - _ => { - // TODO: Show position - warn!("Unable to parse \"{}\"", parsed_text.get_text()); - continue; - } - }; - - let anim_ev = AnimEventColor { - pos, - value: color - }; - - evs.push(anim_ev); - }, - PropKeysEvents::Object(evs) => { - let values = parsed_text.get_values(); - let parsed_values = [ values.get(0), values.get(1) ]; - - let mut anim_ev = AnimEventObject { - pos, - ..Default::default() - }; - - match parsed_values { - [Some(v1), Some(v2)] => { - // First value is usually reserved for milo directory name - anim_ev.text1 = v1.to_string(); - anim_ev.text2 = v2.to_string(); - }, - [Some(v1), ..] => { - anim_ev.text2 = v1.to_string(); - }, - _ => { - // Treat empty array as null symbol - // So do nothing. Maybe further validate symbol syntax? - } - } - - evs.push(anim_ev); - }, - PropKeysEvents::Bool(evs) => { - let anim_ev = AnimEventBool { - pos, - value: match parsed_text.get_values().get(0) { - Some(&"TRUE") => true, - Some(&"FALSE") => false, - _ => { - // TODO: Show position - warn!("Unable to parse \"{}\"", parsed_text.get_text()); - continue; - } - } - }; - - evs.push(anim_ev); - }, - PropKeysEvents::Quat(evs) => { - let quat = match parsed_text.try_parse_values::<4, f32>() { - [Some(x), Some(y), Some(z), Some(w)] => Quat { x, y, z, w }, - _ => { - // TODO: Show position - warn!("Unable to parse \"{}\"", parsed_text.get_text()); - continue; - } - }; - - let anim_ev = AnimEventQuat { - pos, - value: quat - }; - - evs.push(anim_ev); - }, - PropKeysEvents::Vector3(evs) => { - let parsed_values = parsed_text.try_parse_values::<3, f32>(); - - let vector3 = match parsed_values { - [Some(x), Some(y), Some(z)] => Vector3 { x, y, z }, - _ => { - // TODO: Show position - warn!("Unable to parse \"{}\"", parsed_text.get_text()); - continue; - } - }; - - let anim_ev = AnimEventVector3 { - pos, - value: vector3 - }; - - evs.push(anim_ev); - }, - PropKeysEvents::Symbol(evs) => { - let anim_ev = AnimEventSymbol { - pos, - text: parsed_text // Treat empty array as null symbol - .get_values() - .get(0) - .map(|s| s.to_string()) - .unwrap_or_default() - }; - - evs.push(anim_ev); - }, - } - } - - let keys = prop_keys.into_values().collect::>(); - - let (property_count, event_count) = keys - .iter() - .fold( - (0, 0), - |(pc, ec), key| (pc + 1, ec + key.events.len()) - ); - - info!("[{track_name:>10}] Loaded {event_count:>4} events for {property_count:>2} properties"); - - keys -} - -fn load_venue_track(track: &MidiTrack, is_gdrb: bool) -> Vec { - // Property, interpolation, interp_handler, unknown_enum, type - const GDRB_PROPERTIES_VENUE: [(&str, u32, Option<&str>, u32, fn() -> PropKeysEvents); 30] = [ - ("configuration", 0, None, 6, init_events_symbol), - ("crash_ignore_triggers", 0, None, 0, init_events_bool), - ("crowd_anim_override", 0, None, 0, init_events_symbol), - ("crowd_extras_command", 0, None, 0, init_events_symbol), - ("crowd_preset", 0, None, 0, init_events_symbol), - ("floortom_ignore_triggers", 0, None, 0, init_events_bool), - ("hihat_clip", 0, None, 0, init_events_symbol), - ("hihat_ignore_triggers", 0, None, 0, init_events_bool), - ("jumbotron_post_proc", 0, None, 0, init_events_symbol), - ("jumbotron_shot", 0, None, 0, init_events_symbol), - ("kick_ignore_triggers", 0, None, 0, init_events_bool), - ("left_crash_clip", 0, None, 0, init_events_symbol), - ("left_crash_ignore_triggers", 0, None, 0, init_events_bool), - ("left_crash_weight", 1, None, 0, init_events_float), - ("left_floortom_ignore_triggers", 0, None, 0, init_events_bool), - ("left_foot_ignore_triggers", 0, None, 0, init_events_bool), - ("left_tom_ignore_triggers", 0, None, 0, init_events_bool), - ("lighting_preset", 0, None, 0, init_events_symbol), - ("lighting_preset_modifier", 0, None, 0, init_events_symbol), - ("mic_stand_visibility", 0, None, 0, init_events_symbol), - ("postproc", 0, Some("postproc_interp"), 5, init_events_object), - ("postproc_blending_enabled", 0, None, 0, init_events_bool), - ("ride_clip", 0, None, 0, init_events_symbol), - ("ride_ignore_triggers", 0, None, 0, init_events_bool), - ("right_crash_clip", 0, None, 0, init_events_symbol), - ("right_crash_ignore_triggers", 0, None, 0, init_events_bool), - ("right_tom_ignore_triggers", 0, None, 0, init_events_bool), - ("shot", 0, None, 0, init_events_symbol), - ("snare_ignore_triggers", 0, None, 0, init_events_bool), - ("trigger_group", 0, None, 0, init_events_symbol), - ]; - - let venue_properties = if is_gdrb { GDRB_PROPERTIES_VENUE.as_slice() } else { todo!("TBRB venue not supported right now") }; - - load_track(track, venue_properties) -} - -fn load_char_track(track: &MidiTrack, char_name: &str, is_gdrb: bool) -> Vec { - // Property, interpolation, interp_handler, unknown_enum, type - const GDRB_PROPERTIES_CHARS: [(&str, u32, Option<&str>, u32, fn() -> PropKeysEvents); 20] = [ - ("add_face_clip", 0, None, 0, init_events_symbol), - ("add_face_weight", 4, None, 0, init_events_float), - ("attention", 0, None, 0, init_events_symbol), - ("body", 0, None, 0, init_events_symbol), - ("brow_clip", 0, None, 0, init_events_symbol), - ("brow_clip_b", 0, None, 0, init_events_symbol), - ("brow_clip_balance", 1, None, 0, init_events_float), - ("brow_weight", 1, None, 0, init_events_float), - ("face_clip", 0, None, 0, init_events_symbol), - ("face_clip_b", 0, None, 0, init_events_symbol), - ("face_clip_balance", 4, None, 0, init_events_float), - ("face_weight", 4, None, 0, init_events_float), - ("hist_clip", 0, None, 0, init_events_symbol), - ("lid_clip", 0, None, 0, init_events_symbol), - ("lid_clip_b", 0, None, 0, init_events_symbol), - ("lid_clip_balance", 1, None, 0, init_events_float), - ("lid_weight", 1, None, 0, init_events_float), - ("lookat", 1, None, 0, init_events_float), - ("procedural_blink_enabled", 0, None, 0, init_events_bool), - ("vox_clone_enabled", 0, None, 0, init_events_bool), - ]; - - let char_properties = if is_gdrb { GDRB_PROPERTIES_CHARS.as_slice() } else { todo!("TBRB characters not supported right now") }; - - let mut prop_keys = load_track(track, char_properties); - - // Rename properties for specific char - for prop_key in prop_keys.iter_mut() { - let property = prop_key.property.first_mut().map(|p| match p { - DataArray::Symbol(s) => s, - _ => panic!("Shouldn't be hit") - }).unwrap(); - - let transformed_value = match property.as_utf8().unwrap() { - "procedural_blink_enabled" => format!("procedural_blink_{}_enabled", char_name), - "vox_clone_enabled" => format!("vox_clone_{}_enabled", char_name), - default @ _ => format!("{}_{}", default, char_name) - }; - - *property = DataString::from_string(transformed_value); - } - - prop_keys -} - -fn init_events_bool() -> PropKeysEvents { - PropKeysEvents::Bool(Vec::new()) -} - -fn init_events_float() -> PropKeysEvents { - PropKeysEvents::Float(Vec::new()) -} - -fn init_events_object() -> PropKeysEvents { - PropKeysEvents::Object(Vec::new()) -} - -fn init_events_symbol() -> PropKeysEvents { - PropKeysEvents::Symbol(Vec::new()) -} - -fn create_object_dir_for_song(name: &str, info: &SystemInfo) -> ObjectDir { - let mut obj_dir = ObjectDirBase { - name: name.to_string(), - ..ObjectDirBase::new() - }; - - let dir_entry = create_object_dir_entry( - &format!("{name}_ap"), - "song", - &[ - "../../world/shared/director.milo", - "../../world/shared/camera.milo", - &format!("{name}.milo") // Lipsync milo - ], - info - ); - - obj_dir.entries.push(dir_entry.unwrap()); // Uhh... shouldn't fail. All this will be refactored anyways - - ObjectDir::ObjectDir(obj_dir) -} - -fn create_object_dir_for_lipsync(info: &SystemInfo) -> ObjectDir { - let mut obj_dir = ObjectDirBase { - name: String::from("lipsync"), - ..ObjectDirBase::new() - }; - - let dir_entry = create_object_dir_entry( - "lipsync", // Great naming there, HMX - "", - &[], - info - ); - - obj_dir.entries.push(dir_entry.unwrap()); // Uhh... shouldn't fail. All this will be refactored anyways - - ObjectDir::ObjectDir(obj_dir) -} - -fn create_object_dir_entry(name: &str, obj_type: &str, subdir_paths: &[&str], info: &SystemInfo) -> Result> { - // Create stream - let mut data = Vec::::new(); - let mut stream = MemoryStream::from_vector_as_read_write(&mut data); - let mut writer = BinaryStream::from_stream_with_endian(&mut stream, info.endian); - - // Version, revision, type - writer.write_int32(22)?; - writer.write_int32(2)?; - writer.write_prefixed_string(obj_type)?; - - // Viewports - const VIEWPORT_COUNT: i32 = 7; - let mat = Matrix::identity(); - writer.write_int32(VIEWPORT_COUNT)?; - - for _ in 0..VIEWPORT_COUNT { - writer.write_float32(mat.m11)?; - writer.write_float32(mat.m12)?; - writer.write_float32(mat.m13)?; - - writer.write_float32(mat.m21)?; - writer.write_float32(mat.m22)?; - writer.write_float32(mat.m23)?; - - writer.write_float32(mat.m31)?; - writer.write_float32(mat.m32)?; - writer.write_float32(mat.m33)?; - - writer.write_float32(mat.m41)?; - writer.write_float32(mat.m42)?; - writer.write_float32(mat.m43)?; - } - writer.write_int32(0)?; // Current viewport index - - // Inline proxy, proxy file - writer.write_boolean(true)?; - writer.write_prefixed_string("")?; - - // Subdir count, subdirs - writer.write_int32(subdir_paths.len() as i32)?; - for subdir_path in subdir_paths { - writer.write_prefixed_string(subdir_path)?; - } - - // Inline subdir, inline subdir count - writer.write_boolean(false)?; - writer.write_int32(0)?; - - // Unknown strings - writer.write_prefixed_string("")?; - writer.write_prefixed_string("")?; - - // Props (dta). Just ignore for now - writer.write_boolean(false)?; - - // Note - let note = format!("Generated by {} ({})", super::PKG_NAME, super::VERSION); - writer.write_prefixed_string(¬e)?; - - Ok(Object::Packed(PackedObject { - name: name.to_string(), - object_type: String::from("ObjectDir"), - data - })) -} diff --git a/apps/cli/p9_scene_tool/src/formatter.rs b/apps/cli/p9_scene_tool/src/formatter.rs deleted file mode 100644 index 254f821..0000000 --- a/apps/cli/p9_scene_tool/src/formatter.rs +++ /dev/null @@ -1,400 +0,0 @@ -use serde::ser::Serialize; -use serde_json::Result as JsonResult; -use serde_json::ser::{Formatter, PrettyFormatter, Serializer}; -use std::io; - -#[derive(Default)] -pub struct P9Formatter<'a> { - base: PrettyFormatter<'a>, - primitive_array_context: PrimitiveArrayContext, - current_indent: usize, - has_value: bool, - indent: &'a [u8], - dummy_writer: DummyWriter -} - -#[derive(Default)] -struct DummyWriter; - -impl io::Write for DummyWriter { - fn write(&mut self, buf: &[u8]) -> io::Result { - Ok(buf.len()) - } - - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } -} - -// TODO: Support more types -enum PrimitiveArrayContext { - Start, - F32(Vec), - End -} - -impl Default for PrimitiveArrayContext { - fn default() -> Self { - PrimitiveArrayContext::End - } -} - -impl PrimitiveArrayContext { - fn reset(&mut self) { - *self = PrimitiveArrayContext::End - } - - fn begin(&mut self) { - *self = PrimitiveArrayContext::Start - } - - fn at_start(&self) -> bool { - match self { - PrimitiveArrayContext::Start => true, - _ => false - } - } - - fn in_progress(&self) -> bool { - match self { - PrimitiveArrayContext::End => false, - _ => true - } - } - - fn add_f32(&mut self, value: f32) { - if !self.in_progress() { - panic!("Working stream has already ended") - } - - // Init stream - if self.at_start() { - self.init_f32_stream(); - } - - if let Some(stream) = self.get_mut_f32_array() { - stream.push(value) - } else { - panic!("Primitive type doesn't match add") - } - } - - fn init_f32_stream(&mut self) { - if !self.at_start() { - panic!("Can't init stream at already in progress") - } - - *self = PrimitiveArrayContext::F32(Vec::new()) - } - - fn get_mut_f32_array<'a>(&'a mut self) -> Option<&'a mut Vec> { - match self { - PrimitiveArrayContext::F32(stream) => Some(stream), - _ => None - } - } -} - -impl<'a> P9Formatter<'a> { - pub fn new() -> Self { - P9Formatter { - base: PrettyFormatter::new(), - indent: b" ", - ..P9Formatter::default() - } - } - - fn indent(&self, writer: &mut W) -> io::Result<()> { - for _ in 0..self.current_indent { - writer.write_all(self.indent)?; - } - - Ok(()) - } - - fn increment_indent(&mut self) { - self.current_indent += 1; - self.has_value = false; - self.base.begin_array(&mut self.dummy_writer).unwrap(); - } - - fn decrement_indent(&mut self) { - self.current_indent -= 1; - self.base.end_array(&mut self.dummy_writer).unwrap(); - } - - fn cancel_primitive_array(&mut self, writer: &mut W) -> io::Result<()> { - if self.primitive_array_context.in_progress() { - self.primitive_array_context.reset(); - self.base.begin_array_value(writer, !self.has_value) - } else { - Ok(()) - } - } -} - -impl<'a> Formatter for P9Formatter<'a> { - fn write_null(&mut self, writer: &mut W) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.cancel_primitive_array(writer)?; - self.base.write_null(writer) - } - - fn write_bool(&mut self, writer: &mut W, value: bool) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.cancel_primitive_array(writer)?; - self.base.write_bool(writer, value) - } - - fn write_i8(&mut self, writer: &mut W, value: i8) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.cancel_primitive_array(writer)?; - self.base.write_i8(writer, value) - } - - fn write_i16(&mut self, writer: &mut W, value: i16) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.cancel_primitive_array(writer)?; - self.base.write_i16(writer, value) - } - - fn write_i32(&mut self, writer: &mut W, value: i32) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.cancel_primitive_array(writer)?; - self.base.write_i32(writer, value) - } - - fn write_i64(&mut self, writer: &mut W, value: i64) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.cancel_primitive_array(writer)?; - self.base.write_i64(writer, value) - } - - fn write_u8(&mut self, writer: &mut W, value: u8) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.cancel_primitive_array(writer)?; - self.base.write_u8(writer, value) - } - - fn write_u16(&mut self, writer: &mut W, value: u16) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.cancel_primitive_array(writer)?; - self.base.write_u16(writer, value) - } - - fn write_u32(&mut self, writer: &mut W, value: u32) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.cancel_primitive_array(writer)?; - self.base.write_u32(writer, value) - } - - fn write_u64(&mut self, writer: &mut W, value: u64) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.cancel_primitive_array(writer)?; - self.base.write_u64(writer, value) - } - - fn write_f32(&mut self, writer: &mut W, value: f32) -> io::Result<()> - where - W: ?Sized + io::Write, - { - if self.primitive_array_context.in_progress() { - self.primitive_array_context.add_f32(value); - Ok(()) - } else { - self.base.write_f32(writer, value) - } - } - - fn write_f64(&mut self, writer: &mut W, value: f64) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.cancel_primitive_array(writer)?; - self.base.write_f64(writer, value) - } - - fn write_number_str(&mut self, writer: &mut W, value: &str) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.cancel_primitive_array(writer)?; - self.base.write_number_str(writer, value) - } - - fn begin_string(&mut self, writer: &mut W) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.cancel_primitive_array(writer)?; - self.base.begin_string(writer) - } - - fn end_string(&mut self, writer: &mut W) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.base.end_string(writer) - } - - fn write_string_fragment(&mut self, writer: &mut W, fragment: &str) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.cancel_primitive_array(writer)?; - self.base.write_string_fragment(writer, fragment) - } - - fn write_char_escape(&mut self, writer: &mut W, char_escape: serde_json::ser::CharEscape) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.cancel_primitive_array(writer)?; - self.base.write_char_escape(writer, char_escape) - } - - fn begin_array(&mut self, writer: &mut W) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.primitive_array_context.begin(); - self.increment_indent(); - - writer.write_all(b"[") - } - - fn end_array(&mut self, writer: &mut W) -> io::Result<()> - where - W: ?Sized + io::Write, - { - if self.has_value && self.primitive_array_context.in_progress() && !self.primitive_array_context.at_start() { - // Write flat array stream - // TODO: Support other types - if let Some(stream) = self.primitive_array_context.get_mut_f32_array() { - while let Some(value) = stream.pop() { - writer.write_all(b" ")?; - self.base.write_f32(writer, value)?; - - if !stream.is_empty() { - writer.write_all(b",")?; - } else { - writer.write_all(b" ")?; - } - } - } - - self.primitive_array_context.reset(); - self.decrement_indent(); - - writer.write_all(b"]") - } else { - self.current_indent -= 1; - self.primitive_array_context.reset(); - self.base.end_array(writer) - } - } - - fn begin_array_value(&mut self, writer: &mut W, first: bool) -> io::Result<()> - where - W: ?Sized + io::Write, - { - if self.primitive_array_context.in_progress() { - Ok(()) - } else { - self.base.begin_array_value(writer, first) - } - } - - fn end_array_value(&mut self, writer: &mut W) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.has_value = true; - self.base.end_array_value(writer) - } - - fn begin_object(&mut self, writer: &mut W) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.current_indent += 1; - self.has_value = false; - - self.cancel_primitive_array(writer)?; - self.base.begin_object(writer) - } - - fn end_object(&mut self, writer: &mut W) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.current_indent -= 1; - self.base.end_object(writer) - } - - fn begin_object_key(&mut self, writer: &mut W, first: bool) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.base.begin_object_key(writer, first) - } - - fn end_object_key(&mut self, writer: &mut W) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.base.end_object_key(writer) - } - - fn begin_object_value(&mut self, writer: &mut W) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.base.begin_object_value(writer) - } - - fn end_object_value(&mut self, writer: &mut W) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.has_value = true; - self.base.end_object_value(writer) - } - - fn write_raw_fragment(&mut self, writer: &mut W, fragment: &str) -> io::Result<()> - where - W: ?Sized + io::Write, - { - self.base.write_raw_fragment(writer, fragment) - } -} - -pub fn to_string(value: &T) -> JsonResult { - let buffer = Vec::new(); - let formatter = P9Formatter::new(); - - let mut serializer = Serializer::with_formatter(buffer, formatter); - value.serialize(&mut serializer)?; - - Ok(String::from_utf8(serializer.into_inner()).unwrap()) -} \ No newline at end of file diff --git a/apps/cli/p9_scene_tool/src/helpers/mod.rs b/apps/cli/p9_scene_tool/src/helpers/mod.rs deleted file mode 100644 index 93f0168..0000000 --- a/apps/cli/p9_scene_tool/src/helpers/mod.rs +++ /dev/null @@ -1,162 +0,0 @@ -use std::str::FromStr; - - -pub struct FormattedAnimEvent<'a> { - text: &'a str, - property: &'a str, - values: Box<[&'a str]> -} - -impl<'a> FormattedAnimEvent<'a> { - pub fn try_from_str(text: &'a str) -> Option> { - if !is_anim_event(text) { - return None; - } - - let (property, values) = get_property_values(text).unwrap(); - - Some(FormattedAnimEvent { - text, - property, - values: values.into_boxed_slice() - }) - } - - pub fn get_text(&'a self) -> &'a str { - self.text - } - - pub fn get_property(&'a self) -> &'a str { - self.property - } - - pub fn get_values(&'a self) -> &'a [&'a str] { - &self.values - } - - pub fn try_parse_values(&self) -> [Option; N] { - let text_values = self.get_values(); - let mut parsed_values = [None; N]; - - for (i, p) in parsed_values.iter_mut().enumerate() { - *p = text_values.get(i).and_then(|t| t.parse::().ok()); - } - - parsed_values - } -} - -fn is_anim_event(text: &str) -> bool { - if let Some((property, values)) = get_property_values(text) { - are_chars_valid(property) - && values - .iter() - .all(|v| are_chars_valid(v)) - } else { - false - } -} - -fn are_chars_valid(text: &str) -> bool { - const VALID_CHARS: [char; 7] = [ - '.', '_', '-', '[', ']', '(', ')' - ]; - - // Should be [a-zA-Z0-9._[]()]+ - !text.is_empty() && !text - .chars() - .any(|c| !( - c.is_ascii_alphanumeric() - || VALID_CHARS.iter().any(|vc| c.eq(vc)) - )) -} - -fn get_property_values<'a>(text: &'a str) -> Option<(&'a str, Vec<&'a str>)> { - // Extract values from "property (value0 value1 ...)" text - if text.len() < 3 || !text.starts_with('[') || !text.ends_with(']') { - return None; - } - - // Remove [] - let text_no_brackets = &text[1..(text.len() - 1)]; - - // Find whitespace start - let whitespace_start = text_no_brackets.find(|c: char| c.is_whitespace()); - if whitespace_start.is_none() { - return Some((text_no_brackets, Vec::new())); - } - - // Split values separated by whitespace - let (property, mut remaining) = text_no_brackets.split_at(whitespace_start.unwrap()); - remaining = remaining.trim_start(); - - if remaining.is_empty() || !remaining.starts_with('(') || !remaining.ends_with(')') { - // Exit early. Invalid syntax. Don't parse values. - return None; - } - - // Remove () - let remaining_no_brackets = &remaining[1..(remaining.len() - 1)]; - - // Split values in "array" by whitespace - let values = remaining_no_brackets - .split_whitespace() - .collect(); - - Some(( - property, - values - )) -} - -#[cfg(test)] -mod tests { - use rstest::*; - use super::*; - - #[rstest] - #[case("", false)] - #[case("[]", false)] - #[case("[shot CrowdCvStage04.shot]", false)] - #[case("[shot (CrowdCvStage04.shot)]", true)] - #[case("[CrowdCvStage04.shot]", true)] - #[case("[lid_weight (2.0)]", true)] - #[case("[position (2.0 7.0 -4.77778)]", true)] - #[case("[lighting_preset (Back_Spot_Patriot_Strobe_(2fpb).pst)]", true)] - fn test_is_anim_event(#[case] input_text: &str, #[case] expected_result: bool) { - let actual_result = is_anim_event(input_text); - assert_eq!(expected_result, actual_result); - } - - #[rstest] - #[case("", false)] // Empty - #[case(" ", false)] // Whitespace - #[case("420", true)] // Number - #[case("shot", true)] // Lower - #[case("face_clip_balance", true)] // Underscore - #[case("kWaypointConfigLegacy", true)] // Upper - #[case("CrowdCvStage04.shot", true)] // Period - #[case("some-random-thing", true)] // Hyphen (Needed for negative numbers) - #[case("Strobe_(2fpb).pst", true)] // Circle brackets - #[case("Strobe_[2fpb].pst", true)] // Square brackets - fn test_are_char_valid(#[case] input_text: &str, #[case] expected_result: bool) { - let actual_result = are_chars_valid(input_text); - assert_eq!(expected_result, actual_result); - } - - #[rstest] - #[case("some_random_event", None)] - #[case("", None)] // Empty - #[case("[", None)] // Malformed - #[case("[))(", None)] - #[case("[shot (CrowdCvStage04.shot)]", Some(("shot", vec!["CrowdCvStage04.shot"])))] - #[case("[shot (CrowdCvStage04.shot)]", Some(("shot", vec!["CrowdCvStage04.shot"])))] - #[case("[add_face_weight (0.2)]", Some(("add_face_weight", vec!["0.2"])))] - #[case("[attention (FOCUS_Crowd_Interest01.intr)]", Some(("attention", vec!["FOCUS_Crowd_Interest01.intr"])))] - #[case("[position (0.0 5.0 -7.5)]", Some(("position", vec!["0.0", "5.0", "-7.5"])))] - #[case("[lighting_preset (Back_Spot_Patriot_Strobe_(2fpb).pst)]", Some(("lighting_preset", vec!["Back_Spot_Patriot_Strobe_(2fpb).pst"])))] - fn test_get_property_values(#[case] input_text: &str, #[case] expected_result: Option<(&str, Vec<&str>)>) { - let actual_result = get_property_values(input_text); - assert_eq!(expected_result, actual_result); - } -} \ No newline at end of file diff --git a/apps/cli/p9_scene_tool/src/main.rs b/apps/cli/p9_scene_tool/src/main.rs deleted file mode 100644 index dd68a8b..0000000 --- a/apps/cli/p9_scene_tool/src/main.rs +++ /dev/null @@ -1,29 +0,0 @@ -mod apps; -mod formatter; -mod helpers; -mod models; -use apps::P9SceneTool; -use simplelog::*; - -#[cfg(debug_assertions)] -const LOG_LEVEL: LevelFilter = LevelFilter::Debug; - -#[cfg(not(debug_assertions))] -const LOG_LEVEL: LevelFilter = LevelFilter::Info; - -fn main() -> Result<(), Box> { - let log_config = ConfigBuilder::new() - .add_filter_allow_str("grim") - .add_filter_allow_str("p9_scene_tool") - .build(); - - // Setup logging - CombinedLogger::init( - vec![ - TermLogger::new(LOG_LEVEL, log_config, TerminalMode::Mixed, ColorChoice::Auto), - ] - )?; - - let mut scene = P9SceneTool::new(); - scene.run() -} \ No newline at end of file diff --git a/apps/cli/p9_scene_tool/src/models/gdrb.rs b/apps/cli/p9_scene_tool/src/models/gdrb.rs deleted file mode 100644 index dd7a78e..0000000 --- a/apps/cli/p9_scene_tool/src/models/gdrb.rs +++ /dev/null @@ -1,28 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Default, Serialize, Deserialize)] -#[serde(default)] -pub struct GDRBSongPreferences { - pub venue: String, - - pub mike_instruments: Vec, // George - pub billie_instruments: Vec, // John - pub tre_instruments: Vec, // Ringo - - pub tempo: String, - pub song_clips: String, - - // New for GDRB - pub normal_outfit: String, - pub bonus_outfit: String, - pub drum_set: String, - - pub era: String, - - // TODO: Investigate if wanted/needed - #[serde(skip_serializing)] pub cam_directory: String, - #[serde(skip_serializing)] pub media_directory: String, - - pub song_intro_cam: String, - pub win_cam: String, -} \ No newline at end of file diff --git a/apps/cli/p9_scene_tool/src/models/mod.rs b/apps/cli/p9_scene_tool/src/models/mod.rs deleted file mode 100644 index abd100f..0000000 --- a/apps/cli/p9_scene_tool/src/models/mod.rs +++ /dev/null @@ -1,57 +0,0 @@ -mod gdrb; -mod tbrb; -mod serialization; - -pub use gdrb::*; -pub use tbrb::*; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Default)] -pub struct P9Song { - pub name: String, - pub preferences: SongPreferences, - pub lyric_configurations: Option>, -} - -#[derive(Debug)] -pub enum SongPreferences { - TBRB(TBRBSongPreferences), - GDRB(GDRBSongPreferences) -} - -impl Default for SongPreferences { - fn default() -> Self { - SongPreferences::TBRB(TBRBSongPreferences::default()) - } -} - -impl SongPreferences { - pub fn is_tbrb(&self) -> bool { - match self { - SongPreferences::TBRB(_) => true, - _ => false, - } - } - - pub fn is_gdrb(&self) -> bool { - match self { - SongPreferences::GDRB(_) => true, - _ => false, - } - } -} - -#[derive(Debug, Default, Serialize, Deserialize)] -#[serde(default)] -pub struct LyricConfig { - #[serde(alias = "Name")] pub name: String, - #[serde(alias = "Lyrics")] pub lyrics: Vec, -} - -#[derive(Debug, Default, Serialize, Deserialize)] -#[serde(default)] -pub struct LyricEvent { - #[serde(alias = "Pos", rename = "pos")] pub position: [f32; 3], - #[serde(alias = "Rot", rename = "rot")] pub rotation: [f32; 4], - #[serde(alias = "Scale")] pub scale: [f32; 3], -} \ No newline at end of file diff --git a/apps/cli/p9_scene_tool/src/models/serialization.rs b/apps/cli/p9_scene_tool/src/models/serialization.rs deleted file mode 100644 index 363a09d..0000000 --- a/apps/cli/p9_scene_tool/src/models/serialization.rs +++ /dev/null @@ -1,90 +0,0 @@ -use serde::{Deserialize, Serialize, Serializer, ser::{SerializeStruct, SerializeSeq}}; -use serde::de::{self, MapAccess, value::StringDeserializer, Visitor}; -use std::fmt; -use super::*; - -struct P9SongVisitor; - -impl<'de> Visitor<'de> for P9SongVisitor { - type Value = P9Song; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("struct P9Song") - } - - fn visit_map>(self, mut map: V) -> Result { - let mut name: Option = None; - let mut format: Option = None; - let mut preferences: Option = None; - let mut lyric_configs: Option> = None; - - while let Some(key) = map.next_key()? { - match key { - "Name" | "name" => name = map.next_value()?, - "Format" | "format" => format = map.next_value()?, - "Preferences" | "preferences" => { - // TODO: How to handle if "format" is deserialized after preferences? - let prefs = match format.as_deref() { - Some("GDRB") => map - .next_value::() - .map(|gdrb_prefs| SongPreferences::GDRB(gdrb_prefs))?, - _ => map.next_value::() - .map(|tbrb_prefs| SongPreferences::TBRB(tbrb_prefs))?, - }; - - preferences = Some(prefs); - }, - "LyricConfigs" | "lyric_configs" => lyric_configs = map.next_value()?, - _ => continue, - } - } - - Ok(P9Song { - name: name.unwrap_or_default(), - preferences: preferences.unwrap_or_default(), - lyric_configurations: lyric_configs, - ..Default::default() - }) - } -} - -impl<'de> Deserialize<'de> for P9Song { - fn deserialize>(deserializer: D) -> Result { - const FIELDS: [&str; 8] = [ - "Name", "name", - "Format", "format", - "Preferences", "preferences", - "LyricConfigs", "lyric_configs" - ]; - - deserializer.deserialize_struct("P9Song", &FIELDS, P9SongVisitor) - } -} - -impl Serialize for P9Song { - fn serialize(&self, serializer: S) -> Result { - let mut song = serializer.serialize_struct("P9Song", 4)?; - let game_format = match self.preferences { - SongPreferences::TBRB(_) => "TBRB", - SongPreferences::GDRB(_) => "GDRB" - }; - - song.serialize_field("name", &self.name)?; - song.serialize_field("format", game_format)?; - - match &self.preferences { - SongPreferences::TBRB(tbrb_prefs) => { - song.serialize_field("preferences", tbrb_prefs)?; - }, - SongPreferences::GDRB(gdrb_prefs) => { - song.serialize_field("preferences", gdrb_prefs)?; - } - } - - if let Some(lyric_configs) = &self.lyric_configurations { - song.serialize_field("lyric_configs", lyric_configs)?; - } - - song.end() - } -} diff --git a/apps/cli/p9_scene_tool/src/models/tbrb.rs b/apps/cli/p9_scene_tool/src/models/tbrb.rs deleted file mode 100644 index ac0b068..0000000 --- a/apps/cli/p9_scene_tool/src/models/tbrb.rs +++ /dev/null @@ -1,29 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Default, Serialize, Deserialize)] -#[serde(default)] -pub struct TBRBSongPreferences { - #[serde(alias = "Venue")] pub venue: String, - #[serde(alias = "MiniVenues")] pub mini_venues: Vec, - #[serde(alias = "Scenes")] pub scenes: Vec, - - #[serde(alias = "DreamscapeOutfit")] pub dreamscape_outfit: String, - #[serde(alias = "StudioOutfit")] pub studio_outfit: String, - - #[serde(alias = "GeorgeInstruments")] pub george_instruments: Vec, - #[serde(alias = "JohnInstruments")] pub john_instruments: Vec, - #[serde(alias = "PaulInstruments")] pub paul_instruments: Vec, - #[serde(alias = "RingoInstruments")] pub ringo_instruments: Vec, - - #[serde(alias = "Tempo")] pub tempo: String, - #[serde(alias = "SongClips")] pub song_clips: String, - #[serde(alias = "DreamscapeFont")] pub dreamscape_font: String, - - #[serde(alias = "GeorgeAmp")] pub george_amp: String, - #[serde(alias = "JohnAmp")] pub john_amp: String, - #[serde(alias = "PaulAmp")] pub paul_amp: String, - #[serde(alias = "Mixer")] pub mixer: String, - #[serde(alias = "DreamscapeCamera")] pub dreamscape_camera: String, - - #[serde(alias = "LyricPart")] pub lyric_part: String, -} \ No newline at end of file