From fe31f5c595b076bbc2f5c1ff0e72a3473da09f76 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Sun, 2 Nov 2025 10:25:13 +0800 Subject: [PATCH] Add Musica Script (.sc) support --- Cargo.toml | 3 +- README.md | 4 + src/scripts/mod.rs | 4 + src/scripts/musica/mod.rs | 2 + src/scripts/musica/sc.rs | 150 ++++++++++++++++++++++++++++++++++++++ src/types.rs | 3 + 6 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 src/scripts/musica/mod.rs create mode 100644 src/scripts/musica/sc.rs diff --git a/Cargo.toml b/Cargo.toml index 7995c4a..78e1153 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,7 +57,7 @@ zstd = { version = "0.13", optional = true } [features] default = ["all-fmt", "image-jpg", "image-jxl", "image-webp", "audio-flac", "jieba"] all-fmt = ["all-script", "all-img", "all-arc", "all-audio"] -all-script = ["artemis", "artemis-panmimisoft", "bgi", "cat-system", "circus", "entis-gls", "escude", "ex-hibit", "favorite", "hexen-haus", "kirikiri", "silky", "softpal", "will-plus", "yaneurao", "yaneurao-itufuru"] +all-script = ["artemis", "artemis-panmimisoft", "bgi", "cat-system", "circus", "entis-gls", "escude", "ex-hibit", "favorite", "hexen-haus", "kirikiri", "musica", "silky", "softpal", "will-plus", "yaneurao", "yaneurao-itufuru"] all-img = ["bgi-img", "cat-system-img", "circus-img", "emote-img", "hexen-haus-img", "kirikiri-img", "softpal-img", "will-plus-img"] all-arc = ["artemis-arc", "bgi-arc", "cat-system-arc", "circus-arc", "escude-arc", "ex-hibit-arc", "hexen-haus-arc", "kirikiri-arc", "softpal-arc"] all-audio = ["bgi-audio", "circus-audio"] @@ -88,6 +88,7 @@ hexen-haus-img = ["hexen-haus", "image"] kirikiri = ["emote-psb", "fancy-regex", "flate2", "json", "lz4", "utils-escape"] kirikiri-arc = ["kirikiri", "adler", "fastcdc", "flate2", "parse-size", "sha2", "xp3", "zstd"] kirikiri-img = ["kirikiri", "image", "libtlg-rs"] +musica = [] silky = [] softpal = ["int-enum"] softpal-arc = ["softpal"] diff --git a/README.md b/README.md index d2938cf..5985fd6 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,10 @@ msg-tool create -t | Image Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Create | Remarks | |---|---|---|---|---|---|---|---|---| | `kirikiri-tlg`/`kr-tlg` | `kirikiri-img` | Kirikiri TLG Image File (.tlg) | ✔️ | ✔️ | ❌ | ❌ | ✔️ | tlg6 is not supported when importing/creating image | +### Musica +| Script Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Custom Export | Custom Import | Create | Remarks | +|---|---|---|---|---|---|---|---|---|---|---| +| `musica-sc` | `musica` | Musica Script File (.sc) | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | | ### Silky Engine | Script Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Custom Export | Custom Import | Create | Remarks | |---|---|---|---|---|---|---|---|---|---|---| diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index f107500..16902e0 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -22,6 +22,8 @@ pub mod favorite; pub mod hexen_haus; #[cfg(feature = "kirikiri")] pub mod kirikiri; +#[cfg(feature = "musica")] +pub mod musica; #[cfg(feature = "silky")] pub mod silky; #[cfg(feature = "softpal")] @@ -156,6 +158,8 @@ lazy_static::lazy_static! { Box::new(artemis::txt::ArtemisTxtBuilder::new()), #[cfg(feature = "kirikiri-arc")] Box::new(kirikiri::archive::xp3::Xp3ArchiveBuilder::new()), + #[cfg(feature = "musica")] + Box::new(musica::sc::MusicaBuilder::new()), ]; /// A list of all script extensions. pub static ref ALL_EXTS: Vec = diff --git a/src/scripts/musica/mod.rs b/src/scripts/musica/mod.rs new file mode 100644 index 0000000..039babb --- /dev/null +++ b/src/scripts/musica/mod.rs @@ -0,0 +1,2 @@ +//! Musica scripts +pub mod sc; diff --git a/src/scripts/musica/sc.rs b/src/scripts/musica/sc.rs new file mode 100644 index 0000000..f70b4a9 --- /dev/null +++ b/src/scripts/musica/sc.rs @@ -0,0 +1,150 @@ +//! Musica Script (.sc) +use crate::scripts::base::*; +use crate::types::*; +use crate::utils::encoding::*; +use anyhow::Result; +use std::io::Write; + +#[derive(Debug)] +/// Musica Script Builder +pub struct MusicaBuilder {} + +impl MusicaBuilder { + /// Create a new MusicaBuilder + pub fn new() -> Self { + MusicaBuilder {} + } +} + +impl ScriptBuilder for MusicaBuilder { + fn default_encoding(&self) -> Encoding { + Encoding::Cp932 + } + + fn build_script( + &self, + buf: Vec, + _filename: &str, + encoding: Encoding, + _archive_encoding: Encoding, + config: &ExtraConfig, + _archive: Option<&Box>, + ) -> Result> { + Ok(Box::new(MusicaScript::new(buf, encoding, config)?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &["sc"] + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::Musica + } +} + +#[derive(Debug)] +pub struct MusicaScript { + lines: Vec>, +} + +impl MusicaScript { + pub fn new(buf: Vec, encoding: Encoding, _config: &ExtraConfig) -> Result { + let decoded = decode_to_string(encoding, &buf, true)?; + let mut lines = Vec::new(); + for line in decoded.lines() { + let parts: Vec = line.split(' ').map(|s| s.to_string()).collect(); + lines.push(parts); + } + Ok(MusicaScript { lines }) + } +} + +impl Script for MusicaScript { + fn default_output_script_type(&self) -> OutputScriptType { + OutputScriptType::Json + } + + fn default_format_type(&self) -> FormatOptions { + FormatOptions::None + } + + fn extract_messages(&self) -> Result> { + let mut messages = Vec::new(); + for parts in &self.lines { + if parts.is_empty() { + continue; + } + // .message + if parts[0] == ".message" && parts.len() >= 5 { + let name = parts[3].clone(); + let text = parts[4].clone(); + let message = Message { + name: if name.is_empty() { None } else { Some(name) }, + message: text, + }; + messages.push(message); + } + } + Ok(messages) + } + + fn import_messages<'a>( + &'a self, + messages: Vec, + file: Box, + _filename: &str, + encoding: Encoding, + replacement: Option<&'a ReplacementTable>, + ) -> Result<()> { + let mut writer = std::io::BufWriter::new(file); + let mut mes = messages.iter(); + let mut me = mes.next(); + for parts in &self.lines { + let mut parts = parts.clone(); + if parts.is_empty() { + writeln!(writer)?; + continue; + } + if parts[0] == ".message" && parts.len() >= 5 { + let m = match me { + Some(m) => m, + None => return Err(anyhow::anyhow!("Not enough messages to import.")), + }; + if !parts[3].is_empty() { + let mut name = match &m.name { + Some(n) => n.clone(), + None => { + return Err(anyhow::anyhow!( + "Message name is missing for message: {}", + m.message + )); + } + }; + if let Some(repl) = replacement { + for (k, v) in &repl.map { + name = name.replace(k, v); + } + } + parts[3] = name.replace(' ', "\u{3000}"); + } + let mut text = m.message.clone(); + if let Some(repl) = replacement { + for (k, v) in &repl.map { + text = text.replace(k, v); + } + } + parts[4] = text.replace(' ', "\u{3000}"); + me = mes.next(); + } + let line = parts.join(" "); + let d = encode_string(encoding, &line, false)?; + writer.write_all(&d)?; + writeln!(writer)?; + } + if me.is_some() || mes.next().is_some() { + return Err(anyhow::anyhow!("Too many messages to import.")); + } + writer.flush()?; + Ok(()) + } +} diff --git a/src/types.rs b/src/types.rs index daa28c9..4d4fd47 100644 --- a/src/types.rs +++ b/src/types.rs @@ -680,6 +680,9 @@ pub enum ScriptType { #[value(alias("kr-tjs-ns0"))] /// Kirikiri TJS NS0 binary encoded script KirikiriTjsNs0, + #[cfg(feature = "musica")] + /// Musica Script (.sc) + Musica, #[cfg(feature = "silky")] /// Silky Engine Mes script Silky,