From 5555362cea93bae9587be35f9855ee6a9e385892 Mon Sep 17 00:00:00 2001 From: Melody Madeline Lyons Date: Mon, 15 Jul 2024 19:37:13 -0700 Subject: [PATCH] Data conversion (#141) * Conversion that sort of works * Refactor code to be more async * Fix wasm compile error * Oops * Actually deserialize/serialize parameter type now * Add pretty print support * Flush files --- Cargo.lock | 2 + crates/config/src/lib.rs | 8 +- crates/core/src/data_cache.rs | 90 ------- crates/core/src/data_cache/data_formats.rs | 225 +++++++++++++--- crates/core/src/lib.rs | 2 +- crates/data/src/helpers/parameter_type.rs | 47 +++- crates/filesystem/src/project.rs | 7 + crates/ui/Cargo.toml | 2 + crates/ui/src/windows/config_window.rs | 290 ++++++++++++++++++++- 9 files changed, 538 insertions(+), 135 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0a525434..8ea4fd1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3256,10 +3256,12 @@ dependencies = [ name = "luminol-ui" version = "0.4.0" dependencies = [ + "alox-48", "async-std", "camino", "color-eyre", "egui", + "egui-modal", "futures-lite 2.2.0", "futures-util", "indexmap", diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index bc93daf9..e872cb17 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -29,17 +29,17 @@ pub enum DataFormat { #[strum(to_string = "Ruby Marshal")] Marshal, #[strum(to_string = "RON")] - Ron, + Ron { pretty: bool }, #[strum(to_string = "JSON")] - Json, + Json { pretty: bool }, } impl DataFormat { pub fn extension(self) -> &'static str { match self { Self::Marshal => "rxdata", - Self::Ron => "ron", - Self::Json => "json", + Self::Ron { .. } => "ron", + Self::Json { .. } => "json", } } } diff --git a/crates/core/src/data_cache.rs b/crates/core/src/data_cache.rs index c480c2d9..3eb54e03 100644 --- a/crates/core/src/data_cache.rs +++ b/crates/core/src/data_cache.rs @@ -366,96 +366,6 @@ impl Data { } Ok(()) } - - pub fn convert_project( - &mut self, - filesystem: &impl luminol_filesystem::FileSystem, - config: &luminol_config::project::Config, - to: luminol_config::DataFormat, - ) -> color_eyre::Result<()> { - let from_handler = data_formats::Handler::new(config.project.data_format); - let to_handler = data_formats::Handler::new(to); - - let Self::Loaded { - actors, - animations, - armors, - classes, - common_events, - enemies, - items, - map_infos, - scripts, - skills, - states, - system, - tilesets, - troops, - weapons, - .. - } = self - else { - panic!("project not loaded") - }; - - to_handler.write_nil_padded(&actors.get_mut().data, filesystem, "Actors")?; - from_handler.remove_file(filesystem, "Actors")?; - - to_handler.write_nil_padded(&animations.get_mut().data, filesystem, "Animations")?; - from_handler.remove_file(filesystem, "Animations")?; - - to_handler.write_nil_padded(&armors.get_mut().data, filesystem, "Armors")?; - from_handler.remove_file(filesystem, "Armors")?; - - to_handler.write_nil_padded(&classes.get_mut().data, filesystem, "Classes")?; - from_handler.remove_file(filesystem, "Classes")?; - - to_handler.write_nil_padded(&common_events.get_mut().data, filesystem, "CommonEvents")?; - from_handler.remove_file(filesystem, "CommonEvents")?; - - to_handler.write_nil_padded(&enemies.get_mut().data, filesystem, "Enemies")?; - from_handler.remove_file(filesystem, "Enemies")?; - - to_handler.write_nil_padded(&items.get_mut().data, filesystem, "Items")?; - from_handler.remove_file(filesystem, "Items")?; - - to_handler.write_nil_padded(&skills.get_mut().data, filesystem, "Skills")?; - from_handler.remove_file(filesystem, "Skills")?; - - to_handler.write_nil_padded(&states.get_mut().data, filesystem, "States")?; - from_handler.remove_file(filesystem, "States")?; - - to_handler.write_nil_padded(&tilesets.get_mut().data, filesystem, "Tilesets")?; - from_handler.remove_file(filesystem, "Tilesets")?; - - to_handler.write_nil_padded(&troops.get_mut().data, filesystem, "Troops")?; - from_handler.remove_file(filesystem, "Troops")?; - - to_handler.write_nil_padded(&weapons.get_mut().data, filesystem, "Weapons")?; - from_handler.remove_file(filesystem, "Weapons")?; - - // special handling - to_handler.write_data( - &scripts.get_mut().data, - filesystem, - &config.project.scripts_path, - )?; - from_handler.remove_file(filesystem, &config.project.scripts_path)?; - - to_handler.write_data(&system.get_mut(), filesystem, "System")?; - from_handler.remove_file(filesystem, "System")?; - - to_handler.write_data(&map_infos.get_mut().data, filesystem, "MapInfos")?; - from_handler.remove_file(filesystem, "MapInfos")?; - - for id in map_infos.get_mut().data.keys() { - let map: rpg::Map = from_handler.read_data(filesystem, format!("Map{id:0>3}"))?; - to_handler.write_data(&map, filesystem, format!("Map{id:0>3}"))?; - from_handler.remove_file(filesystem, format!("Map{id:0>3}"))?; - } - - Ok(()) - } } macro_rules! nested_ref_getter { diff --git a/crates/core/src/data_cache/data_formats.rs b/crates/core/src/data_cache/data_formats.rs index 37d18722..b60dd778 100644 --- a/crates/core/src/data_cache/data_formats.rs +++ b/crates/core/src/data_cache/data_formats.rs @@ -36,6 +36,12 @@ impl Handler { Self { format } } + pub fn path_for(self, filename: impl AsRef) -> camino::Utf8PathBuf { + camino::Utf8Path::new("Data") + .join(filename) + .with_extension(self.format.extension()) + } + pub fn read_data( self, filesystem: &impl luminol_filesystem::FileSystem, @@ -45,10 +51,7 @@ impl Handler { T: for<'de> alox_48::Deserialize<'de>, T: ::serde::de::DeserializeOwned, { - let path = camino::Utf8Path::new("Data") - .join(filename) - .with_extension(self.format.extension()); - let data = filesystem.read(path)?; + let data = filesystem.read(self.path_for(filename))?; match self.format { DataFormat::Marshal => { @@ -57,17 +60,40 @@ impl Handler { result.map_err(|(error, trace)| format_traced_error(error, trace)) } - DataFormat::Ron => { + DataFormat::Ron { .. } => { let mut de = ron::de::Deserializer::from_bytes(&data)?; serde_path_to_error::deserialize(&mut de).map_err(format_path_to_error) } - DataFormat::Json => { + DataFormat::Json { .. } => { let mut de = serde_json::de::Deserializer::from_slice(&data); serde_path_to_error::deserialize(&mut de).map_err(format_path_to_error) } } } + pub fn read_data_from(self, data: &[u8]) -> color_eyre::Result + where + T: for<'de> alox_48::Deserialize<'de>, + T: ::serde::de::DeserializeOwned, + { + match self.format { + DataFormat::Marshal => { + let mut de = alox_48::Deserializer::new(data)?; + let result = alox_48::path_to_error::deserialize(&mut de); + + result.map_err(|(error, trace)| format_traced_error(error, trace)) + } + DataFormat::Ron { .. } => { + let mut de = ron::de::Deserializer::from_bytes(data)?; + serde_path_to_error::deserialize(&mut de).map_err(format_path_to_error) + } + DataFormat::Json { .. } => { + let mut de = serde_json::de::Deserializer::from_slice(data); + serde_path_to_error::deserialize(&mut de).map_err(format_path_to_error) + } + } + } + pub fn write_data( self, data: &T, @@ -78,11 +104,8 @@ impl Handler { T: ::serde::Serialize, T: alox_48::Serialize, { - let path = camino::Utf8Path::new("Data") - .join(filename) - .with_extension(self.format.extension()); let mut file = filesystem.open_file( - path, + self.path_for(filename), OpenFlags::Create | OpenFlags::Truncate | OpenFlags::Write, )?; @@ -93,14 +116,53 @@ impl Handler { .map_err(|(error, trace)| format_traced_error(error, trace))?; file.write_all(&serializer.output)?; } - DataFormat::Ron => { - let mut ser = ron::Serializer::new(&mut file, None)?; + DataFormat::Ron { pretty } => { + let config = pretty.then(|| ron::ser::PrettyConfig::new().struct_names(true)); + let mut ser = + ron::Serializer::with_options(&mut file, config, ron::Options::default())?; serde_path_to_error::serialize(data, &mut ser)?; } - DataFormat::Json => { - let mut ser = serde_json::Serializer::new(&mut file); + DataFormat::Json { pretty } => { + if pretty { + let mut ser = serde_json::Serializer::pretty(&mut file); + serde_path_to_error::serialize(data, &mut ser)?; + } else { + let mut ser = serde_json::Serializer::new(&mut file); + serde_path_to_error::serialize(data, &mut ser)?; + } + } + }; + + Ok(()) + } + + pub fn write_data_to(self, data: &T, buffer: &mut Vec) -> color_eyre::Result<()> + where + T: ::serde::Serialize, + T: alox_48::Serialize, + { + match self.format { + DataFormat::Marshal => { + let mut serializer = alox_48::Serializer::new(); + alox_48::path_to_error::serialize(data, &mut serializer) + .map_err(|(error, trace)| format_traced_error(error, trace))?; + buffer.extend_from_slice(&serializer.output); + } + DataFormat::Ron { pretty } => { + let config = pretty.then(|| ron::ser::PrettyConfig::new().struct_names(true)); + let mut ser = + ron::Serializer::with_options(buffer, config, ron::Options::default())?; serde_path_to_error::serialize(data, &mut ser)?; } + DataFormat::Json { pretty } => { + if pretty { + let mut ser = serde_json::Serializer::pretty(buffer); + serde_path_to_error::serialize(data, &mut ser)?; + } else { + let mut ser = serde_json::Serializer::new(buffer); + serde_path_to_error::serialize(data, &mut ser)?; + } + } }; Ok(()) @@ -115,10 +177,7 @@ impl Handler { T: for<'de> alox_48::Deserialize<'de>, T: ::serde::de::DeserializeOwned, { - let path = camino::Utf8Path::new("Data") - .join(filename) - .with_extension(self.format.extension()); - let data = filesystem.read(path)?; + let data = filesystem.read(self.path_for(filename))?; match self.format { DataFormat::Marshal => { @@ -129,7 +188,7 @@ impl Handler { luminol_data::helpers::nil_padded_alox::deserialize_with(de) .map_err(|error| format_traced_error(error, trace)) } - DataFormat::Ron => { + DataFormat::Ron { .. } => { let mut de = ron::de::Deserializer::from_bytes(&data)?; let mut track = serde_path_to_error::Track::new(); let de = serde_path_to_error::Deserializer::new(&mut de, &mut track); @@ -139,7 +198,7 @@ impl Handler { format_path_to_error(error) }) } - DataFormat::Json => { + DataFormat::Json { .. } => { let mut de = serde_json::de::Deserializer::from_slice(&data); let mut track = serde_path_to_error::Track::new(); let de = serde_path_to_error::Deserializer::new(&mut de, &mut track); @@ -152,6 +211,43 @@ impl Handler { } } + pub fn read_nil_padded_from(self, data: &[u8]) -> color_eyre::Result> + where + T: for<'de> alox_48::Deserialize<'de>, + T: ::serde::de::DeserializeOwned, + { + match self.format { + DataFormat::Marshal => { + let mut de = alox_48::Deserializer::new(data)?; + let mut trace = alox_48::path_to_error::Trace::default(); + let de = alox_48::path_to_error::Deserializer::new(&mut de, &mut trace); + + luminol_data::helpers::nil_padded_alox::deserialize_with(de) + .map_err(|error| format_traced_error(error, trace)) + } + DataFormat::Ron { .. } => { + let mut de = ron::de::Deserializer::from_bytes(data)?; + let mut track = serde_path_to_error::Track::new(); + let de = serde_path_to_error::Deserializer::new(&mut de, &mut track); + + luminol_data::helpers::nil_padded_serde::deserialize(de).map_err(|inner| { + let error = serde_path_to_error::Error::new(track.path(), inner); + format_path_to_error(error) + }) + } + DataFormat::Json { .. } => { + let mut de = serde_json::de::Deserializer::from_slice(data); + let mut track = serde_path_to_error::Track::new(); + let de = serde_path_to_error::Deserializer::new(&mut de, &mut track); + + luminol_data::helpers::nil_padded_serde::deserialize(de).map_err(|inner| { + let error = serde_path_to_error::Error::new(track.path(), inner); + format_path_to_error(error) + }) + } + } + } + pub fn write_nil_padded( self, data: &[T], @@ -162,11 +258,8 @@ impl Handler { T: ::serde::Serialize, T: alox_48::Serialize, { - let path = camino::Utf8Path::new("Data") - .join(filename) - .with_extension(self.format.extension()); let mut file = filesystem.open_file( - path, + self.path_for(filename), OpenFlags::Create | OpenFlags::Truncate | OpenFlags::Write, )?; @@ -180,9 +273,35 @@ impl Handler { .map_err(|error| format_traced_error(error, trace))?; file.write_all(&ser.output)?; } - DataFormat::Json => { + DataFormat::Json { pretty } => { let mut track = serde_path_to_error::Track::new(); - let mut ser = serde_json::Serializer::new(&mut file); + if pretty { + let mut ser = serde_json::Serializer::pretty(&mut file); + let ser = serde_path_to_error::Serializer::new(&mut ser, &mut track); + + luminol_data::helpers::nil_padded_serde::serialize(data, ser).map_err( + |inner| { + let error = serde_path_to_error::Error::new(track.path(), inner); + format_path_to_error(error) + }, + )?; + } else { + let mut ser = serde_json::Serializer::new(&mut file); + let ser = serde_path_to_error::Serializer::new(&mut ser, &mut track); + + luminol_data::helpers::nil_padded_serde::serialize(data, ser).map_err( + |inner| { + let error = serde_path_to_error::Error::new(track.path(), inner); + format_path_to_error(error) + }, + )?; + } + } + DataFormat::Ron { pretty } => { + let mut track = serde_path_to_error::Track::new(); + let config = pretty.then(|| ron::ser::PrettyConfig::new().struct_names(true)); + let mut ser = + ron::Serializer::with_options(&mut file, config, ron::Options::default())?; let ser = serde_path_to_error::Serializer::new(&mut ser, &mut track); luminol_data::helpers::nil_padded_serde::serialize(data, ser).map_err(|inner| { @@ -190,9 +309,57 @@ impl Handler { format_path_to_error(error) })?; } - DataFormat::Ron => { + } + + file.flush()?; + + Ok(()) + } + + pub fn write_nil_padded_to(self, data: &[T], buffer: &mut Vec) -> color_eyre::Result<()> + where + T: ::serde::Serialize, + T: alox_48::Serialize, + { + match self.format { + DataFormat::Marshal => { + let mut trace = alox_48::path_to_error::Trace::new(); + let mut ser = alox_48::Serializer::new(); + let trace_ser = alox_48::path_to_error::Serializer::new(&mut ser, &mut trace); + + luminol_data::helpers::nil_padded_alox::serialize_with(data, trace_ser) + .map_err(|error| format_traced_error(error, trace))?; + buffer.extend_from_slice(&ser.output); + } + DataFormat::Json { pretty } => { let mut track = serde_path_to_error::Track::new(); - let mut ser = ron::Serializer::new(&mut file, None)?; + if pretty { + let mut ser = serde_json::Serializer::pretty(buffer); + let ser = serde_path_to_error::Serializer::new(&mut ser, &mut track); + + luminol_data::helpers::nil_padded_serde::serialize(data, ser).map_err( + |inner| { + let error = serde_path_to_error::Error::new(track.path(), inner); + format_path_to_error(error) + }, + )?; + } else { + let mut ser = serde_json::Serializer::new(buffer); + let ser = serde_path_to_error::Serializer::new(&mut ser, &mut track); + + luminol_data::helpers::nil_padded_serde::serialize(data, ser).map_err( + |inner| { + let error = serde_path_to_error::Error::new(track.path(), inner); + format_path_to_error(error) + }, + )?; + } + } + DataFormat::Ron { pretty } => { + let mut track = serde_path_to_error::Track::new(); + let config = pretty.then(|| ron::ser::PrettyConfig::new().struct_names(true)); + let mut ser = + ron::Serializer::with_options(buffer, config, ron::Options::default())?; let ser = serde_path_to_error::Serializer::new(&mut ser, &mut track); luminol_data::helpers::nil_padded_serde::serialize(data, ser).map_err(|inner| { @@ -202,8 +369,6 @@ impl Handler { } } - file.flush()?; - Ok(()) } diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index de51f24c..17293050 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -46,7 +46,7 @@ pub use project_manager::spawn_future; pub use project_manager::ProjectManager; pub use alox_48; -pub use data_cache::data_formats::format_traced_error; +pub use data_cache::data_formats::{self, format_traced_error}; pub mod prelude { pub use crate::{Modal, Tab, UpdateState, Window}; diff --git a/crates/data/src/helpers/parameter_type.rs b/crates/data/src/helpers/parameter_type.rs index 169cf2e0..48fc206b 100644 --- a/crates/data/src/helpers/parameter_type.rs +++ b/crates/data/src/helpers/parameter_type.rs @@ -22,6 +22,8 @@ // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. +use alox_48::Value; + use crate::rgss_structs::{Color, Tone}; use crate::shared::{AudioFile, MoveCommand, MoveRoute}; @@ -36,7 +38,7 @@ pub enum ParameterType { Color(Color), Tone(Tone), AudioFile(AudioFile), - Float(f32), + Float(f64), MoveRoute(MoveRoute), MoveCommand(MoveCommand), Array(Vec), @@ -48,15 +50,46 @@ pub enum ParameterType { // FIXME this really should be try_from and try_into impl From for ParameterType { - // This is a massive sore spot right now, so I'm not even bothering... - fn from(_value: alox_48::Value) -> Self { - ParameterType::None + fn from(value: alox_48::Value) -> Self { + match value { + Value::Nil => Self::None, + Value::Integer(v) => Self::Integer(v), + Value::Float(v) => Self::Float(v), + Value::String(v) => Self::String(String::from_utf8(v.data).unwrap()), + Value::Array(v) => Self::Array(v.into_iter().map(|v| v.into()).collect()), + Value::Bool(v) => Self::Bool(v), + Value::Userdata(userdata) => match userdata.class.as_str() { + "Color" => Self::Color(Color::from(userdata)), + "Tone" => Self::Tone(Tone::from(userdata)), + _ => panic!("Unsupported userdata type: {:#?}", userdata), + }, + Value::Object(alox_48::Object { ref class, .. }) => match class.as_str() { + "RPG::AudioFile" => Self::AudioFile(alox_48::from_value(&value).unwrap()), + "RPG::MoveRoute" => Self::MoveRoute(alox_48::from_value(&value).unwrap()), + "RPG::MoveCommand" => Self::MoveCommand(alox_48::from_value(&value).unwrap()), + _ => panic!("Unsupported object type: {:#?}", value), + }, + Value::Instance(i) => (*i.value).into(), + _ => panic!("Unsupported value type: {:#?}", value), + } } } impl From for alox_48::Value { - fn from(_value: ParameterType) -> Self { - alox_48::Value::Nil + fn from(value: ParameterType) -> Self { + match value { + ParameterType::None => Value::Nil, + ParameterType::Integer(v) => Value::Integer(v), + ParameterType::Float(v) => Value::Float(v), + ParameterType::String(v) => Value::String(v.into()), + ParameterType::Array(v) => Value::Array(v.into_iter().map(|v| v.into()).collect()), + ParameterType::Bool(v) => Value::Bool(v), + ParameterType::Color(v) => Value::Userdata(v.into()), + ParameterType::Tone(v) => Value::Userdata(v.into()), + ParameterType::AudioFile(v) => alox_48::to_value(v).unwrap(), + ParameterType::MoveRoute(v) => alox_48::to_value(v).unwrap(), + ParameterType::MoveCommand(v) => alox_48::to_value(v).unwrap(), + } } } @@ -153,7 +186,7 @@ variant_impl! { Color, Color, Tone, Tone, AudioFile, AudioFile, - Float, f32, + Float, f64, MoveRoute, MoveRoute, MoveCommand, MoveCommand, Array, Vec, diff --git a/crates/filesystem/src/project.rs b/crates/filesystem/src/project.rs index bdb53d07..73d1f39a 100644 --- a/crates/filesystem/src/project.rs +++ b/crates/filesystem/src/project.rs @@ -65,6 +65,13 @@ impl FileSystem { pub fn unload_project(&mut self) { *self = FileSystem::Unloaded; } + + pub fn rebuild_path_cache(&mut self) { + let FileSystem::Loaded { filesystem, .. } = self else { + return; + }; + filesystem.rebuild(); + } } // Not platform specific diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index fdaa4326..71922831 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -28,6 +28,7 @@ luminol-modals.workspace = true luminol-macros.workspace = true egui.workspace = true +egui-modal = "0.3.6" camino.workspace = true @@ -62,6 +63,7 @@ wgpu.workspace = true murmur3.workspace = true indexmap = "2.2.6" +alox-48.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] luminol-term = { version = "0.4.0", path = "../term/" } diff --git a/crates/ui/src/windows/config_window.rs b/crates/ui/src/windows/config_window.rs index 7cc2fd38..405ae163 100644 --- a/crates/ui/src/windows/config_window.rs +++ b/crates/ui/src/windows/config_window.rs @@ -22,21 +22,242 @@ // terms of the Steamworks API by Valve Corporation, the licensors of this // Program grant you additional permission to convey the resulting work. +use async_std::io::{ReadExt, WriteExt}; +use egui::Widget; +use luminol_core::data_formats::Handler as FormatHandler; +use luminol_data::rpg; +use luminol_filesystem::{FileSystem, OpenFlags}; +use std::sync::atomic::{AtomicUsize, Ordering}; use strum::IntoEnumIterator; pub struct Window { selected_data_format: luminol_config::DataFormat, + convert: Option, +} + +struct Convert { + promise: poll_promise::Promise>, + converting: Converting, } impl Window { pub fn new(config: &luminol_config::project::Config) -> Self { Self { selected_data_format: config.project.data_format, + convert: None, } } } -const FORMAT_WARNING: &str = "While the option is provided,\nLuminol cannot convert between formats yet.\nIt can still read other formats, however."; // "Luminol will need to convert your project.\nThis is not 100% safe yet, make backups!\nPress OK to continue."; +type Converting = std::sync::Arc<(AtomicUsize, AtomicUsize)>; + +const CONVERTING_ACTORS: usize = 0; +const CONVERTING_ANIMATIONS: usize = 1; +const CONVERTING_ARMORS: usize = 2; +const CONVERTING_CLASSES: usize = 3; +const CONVERTING_COMMON_EVENTS: usize = 4; +const CONVERTING_ENEMIES: usize = 5; +const CONVERTING_ITEMS: usize = 6; +const CONVERTING_SKILLS: usize = 7; +const CONVERTING_STATES: usize = 8; +const CONVERTING_TILESETS: usize = 9; +const CONVERTING_TROOPS: usize = 10; +const CONVERTING_WEAPONS: usize = 11; +const CONVERTING_SCRIPTS: usize = 12; +const CONVERTING_SYSTEM: usize = 13; +const CONVERTING_MAPINFOS: usize = 14; + +fn converting_to_string( + converting: usize, + map_id: usize, + selected_data_format: luminol_config::DataFormat, +) -> String { + let text = match converting { + CONVERTING_ACTORS => "Actors", + CONVERTING_ANIMATIONS => "Animations", + CONVERTING_ARMORS => "Armors", + CONVERTING_CLASSES => "Classes", + CONVERTING_COMMON_EVENTS => "CommonEvents", + CONVERTING_ENEMIES => "Enemies", + CONVERTING_ITEMS => "Items", + CONVERTING_SKILLS => "Skills", + CONVERTING_STATES => "States", + CONVERTING_TILESETS => "Tilesets", + CONVERTING_TROOPS => "Troops", + CONVERTING_WEAPONS => "Weapons", + + CONVERTING_SCRIPTS => "Scripts", + CONVERTING_SYSTEM => "System", + CONVERTING_MAPINFOS => "MapInfos", + _ => { + return format!("Map{map_id:0>3}.{}", selected_data_format.extension()); + } + }; + format!("{}.{}", text, selected_data_format.extension()) +} + +const FORMAT_WARNING: &str = "Luminol will need to convert your project.\nThis is not 100% safe yet, make backups!\nPress OK to continue."; + +// Mostly async, opening files is not however. +// We should probably provide async fns for that +async fn convert_nil_padded( + from: FormatHandler, + to: FormatHandler, + read_buf: &mut Vec, + write_buf: &mut Vec, + filename: &str, + host: &luminol_filesystem::host::FileSystem, +) -> color_eyre::Result> +where + T: ::serde::de::DeserializeOwned + serde::Serialize, + T: for<'de> alox_48::Deserialize<'de> + alox_48::Serialize, +{ + read_buf.clear(); + write_buf.clear(); + + let mut file = host.open_file(from.path_for(filename), OpenFlags::Read)?; + file.read_to_end(read_buf).await?; + + let data = from.read_nil_padded_from::(read_buf)?; + + to.write_nil_padded_to(&data, write_buf)?; + + let mut file = host.open_file( + to.path_for(filename), + OpenFlags::Write | OpenFlags::Truncate | OpenFlags::Create, + )?; + file.write_all(write_buf).await?; + file.flush().await?; + + from.remove_file(host, filename)?; + + Ok(data) +} + +async fn convert_regular( + from: FormatHandler, + to: FormatHandler, + read_buf: &mut Vec, + write_buf: &mut Vec, + filename: &str, + host: &luminol_filesystem::host::FileSystem, +) -> color_eyre::Result +where + T: ::serde::de::DeserializeOwned + serde::Serialize, + T: for<'de> alox_48::Deserialize<'de> + alox_48::Serialize, +{ + read_buf.clear(); + write_buf.clear(); + + let mut file = host.open_file(from.path_for(filename), OpenFlags::Read)?; + file.read_to_end(read_buf).await?; + + let data = from.read_data_from::(read_buf)?; + + to.write_data_to(&data, write_buf)?; + + let mut file = host.open_file( + to.path_for(filename), + OpenFlags::Write | OpenFlags::Truncate | OpenFlags::Create, + )?; + file.write_all(write_buf).await?; + file.flush().await?; + + from.remove_file(host, filename)?; + + Ok(data) +} + +fn convert_project( + config: &mut luminol_config::project::Config, + selected_data_format: luminol_config::DataFormat, + filesystem: &luminol_filesystem::project::FileSystem, + converting: Converting, +) -> impl std::future::Future> { + let from = FormatHandler::new(config.project.data_format); + let to = FormatHandler::new(selected_data_format); + + // TODO handle errors + let pretty_config = ron::ser::PrettyConfig::new() + .struct_names(true) + .enumerate_arrays(true); + config.project.data_format = selected_data_format; + let project_config = ron::ser::to_string_pretty(&config.project, pretty_config).unwrap(); + filesystem.write(".luminol/config", project_config).unwrap(); + + let host = filesystem.host().unwrap(); // This bypasses the path cache (which is BAD!) so we will need to regen it later + let scripts_filename = config.project.scripts_path.clone(); + + async move { + let mut read_buf = Vec::new(); + let mut write_buf = Vec::::new(); + + let host = &host; + let read_buf = &mut read_buf; + let write_buf = &mut write_buf; + + let (converting_progress, converting_map_id) = &*converting; + + // FIXME: have some kind of trait system to determine filenames rather than hardcoding them like this + converting_progress.store(CONVERTING_ACTORS, Ordering::Relaxed); + convert_nil_padded::(from, to, read_buf, write_buf, "Actors", host).await?; + + converting_progress.store(CONVERTING_ANIMATIONS, Ordering::Relaxed); + convert_nil_padded::(from, to, read_buf, write_buf, "Animations", host) + .await?; + + converting_progress.store(CONVERTING_ARMORS, Ordering::Relaxed); + convert_nil_padded::(from, to, read_buf, write_buf, "Armors", host).await?; + + converting_progress.store(CONVERTING_CLASSES, Ordering::Relaxed); + convert_nil_padded::(from, to, read_buf, write_buf, "Classes", host).await?; + + converting_progress.store(CONVERTING_COMMON_EVENTS, Ordering::Relaxed); + convert_nil_padded::(from, to, read_buf, write_buf, "CommonEvents", host) + .await?; + + converting_progress.store(CONVERTING_ENEMIES, Ordering::Relaxed); + convert_nil_padded::(from, to, read_buf, write_buf, "Enemies", host).await?; + + converting_progress.store(CONVERTING_ITEMS, Ordering::Relaxed); + convert_nil_padded::(from, to, read_buf, write_buf, "Items", host).await?; + + converting_progress.store(CONVERTING_SKILLS, Ordering::Relaxed); + convert_nil_padded::(from, to, read_buf, write_buf, "Skills", host).await?; + + converting_progress.store(CONVERTING_STATES, Ordering::Relaxed); + convert_nil_padded::(from, to, read_buf, write_buf, "States", host).await?; + + converting_progress.store(CONVERTING_TILESETS, Ordering::Relaxed); + convert_nil_padded::(from, to, read_buf, write_buf, "Tilesets", host).await?; + + converting_progress.store(CONVERTING_TROOPS, Ordering::Relaxed); + convert_nil_padded::(from, to, read_buf, write_buf, "Troops", host).await?; + + converting_progress.store(CONVERTING_WEAPONS, Ordering::Relaxed); + convert_nil_padded::(from, to, read_buf, write_buf, "Weapons", host).await?; + + converting_progress.store(CONVERTING_SCRIPTS, Ordering::Relaxed); + convert_regular::>(from, to, read_buf, write_buf, &scripts_filename, host) + .await?; + + converting_progress.store(CONVERTING_SYSTEM, Ordering::Relaxed); + convert_regular::(from, to, read_buf, write_buf, "System", host).await?; + + converting_progress.store(CONVERTING_MAPINFOS, Ordering::Relaxed); + let mapinfos: std::collections::HashMap = + convert_regular(from, to, read_buf, write_buf, "MapInfos", host).await?; + + for (index, map_id) in mapinfos.keys().copied().enumerate() { + converting_progress.store(CONVERTING_MAPINFOS + index, Ordering::Relaxed); + converting_map_id.store(map_id, Ordering::Relaxed); + + let map_filename = format!("Map{map_id:0>3}"); + convert_regular::(from, to, read_buf, write_buf, &map_filename, host).await?; + } + Ok(()) + } +} impl luminol_core::Window for Window { fn id(&self) -> egui::Id { @@ -96,6 +317,11 @@ impl luminol_core::Window for Window { .changed(); } }); + if let luminol_config::DataFormat::Json { pretty } + | luminol_config::DataFormat::Ron { pretty } = &mut self.selected_data_format + { + ui.checkbox(pretty, "Pretty Print").on_hover_text("This will make the data files human-readable, but significantly larger!"); + } if self.selected_data_format != config.project.data_format { // add warning message about needing to edit every single data file @@ -126,8 +352,21 @@ impl luminol_core::Window for Window { ) .clicked(); if clicked { - config.project.data_format = self.selected_data_format; - // TODO add conversion logic + let converting = Converting::default(); + let future = convert_project( + config, + self.selected_data_format, + update_state.filesystem, + converting.clone(), + ); + #[cfg(not(target_arch = "wasm32"))] + let promise = poll_promise::Promise::spawn_async(future); + #[cfg(target_arch = "wasm32")] + let promise = poll_promise::Promise::spawn_local(future); + self.convert = Some(Convert { + promise, + converting, + }); } } @@ -187,6 +426,51 @@ impl luminol_core::Window for Window { }); }); + if let Some(convert) = self.convert.take() { + let modal = egui_modal::Modal::new(ctx, "converting_project_modal"); + modal.show(|ui| { + modal.title(ui, "Converting Project..."); + + let map_infos = update_state.data.map_infos(); + let map_count = map_infos.data.len(); + + let current_progress = convert.converting.0.load(Ordering::Relaxed); + let current_map_id = convert.converting.1.load(Ordering::Relaxed); + + let total = CONVERTING_MAPINFOS + map_count + 1; + let progress = (current_progress + 2) as f32 / total as f32; + + let current_text = converting_to_string( + current_progress, + current_map_id, + self.selected_data_format, + ); + + ui.label(format!( + "Converting {current_text} {}/{total}", + current_progress + 2 + )); + egui::ProgressBar::new(progress).animate(true).ui(ui); + }); + match convert.promise.try_take() { + Ok(Ok(())) => { + // we've drastically edited the data folder, so the path cache needs to be rebuilt + update_state.filesystem.rebuild_path_cache(); + } + Ok(Err(err)) => { + luminol_core::error!(update_state.toasts, err); + luminol_core::warn!( + update_state.toasts, + "WARNING: Your project may be corrupted!" + ); + } + Err(promise) => { + modal.open(); + self.convert = Some(Convert { promise, ..convert }); + } + } + } + if modified { update_state.modified.set(true); }