From 5898d8f94e3b35c39d305a5ffefd57fde417fba9 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Sun, 17 Aug 2025 17:19:13 +0800 Subject: [PATCH] Add artemis txt script support --- README.md | 1 + src/args.rs | 15 + src/main.rs | 6 + src/scripts/artemis/mod.rs | 1 + src/scripts/artemis/txt.rs | 1216 ++++++++++++++++++++++++++++++++++++ src/scripts/mod.rs | 2 + src/types.rs | 11 + 7 files changed, 1252 insertions(+) create mode 100644 src/scripts/artemis/txt.rs diff --git a/README.md b/README.md index 9f4b2e3..c97a511 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ msg-tool create -t |---|---|---|---|---|---|---|---|---| | `artemis` | `artemis` | Artemis Engine AST file (.ast) | ✔️ | ✔️ | ❌ | ❌ | ❌ | | | `artemis-asb` | `artemis` | Artemis Engine ASB file (.asb) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | | +| `artemis-txt` | `artemis` | Artemis Engine TXT file (.txt) | ✔️ | ✔️ | ❌ | ❌ | ❌ | | | Archive Type | Feature Name | Name | Unpack | Pack | Remarks | |---|---|---|---|---|---| diff --git a/src/args.rs b/src/args.rs index 86721a7..95cf443 100644 --- a/src/args.rs +++ b/src/args.rs @@ -240,6 +240,21 @@ pub struct Arg { /// Specify the language of Artemis AST script. /// If not specified, the first language will be used. pub artemis_ast_lang: Option, + #[cfg(feature = "artemis")] + #[arg( + long, + global = true, + value_delimiter = ',', + default_value = "遅延イベントCG,遅延背景,bgv_in,イベントCG,遅延ポップアップ" + )] + /// Artemis Engine blacklist tag names for TXT script. + /// This is used to ignore these tags when finding names in Artemis TXT script. + pub artemis_txt_blacklist_names: Vec, + #[cfg(feature = "artemis")] + #[arg(long, global = true)] + /// Specify the language of Artemis TXT script. + /// If not specified, the first language will be used. + pub artemis_txt_lang: Option, #[cfg(feature = "cat-system")] #[arg(long, global = true)] /// CatSystem2 CSTL script language, used to extract messages from CSTL script. diff --git a/src/main.rs b/src/main.rs index 0d3d7d8..65b3f0c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1763,6 +1763,12 @@ fn main() { entis_gls_srcxml_lang: arg.entis_gls_srcxml_lang.clone(), #[cfg(feature = "will-plus")] will_plus_ws2_no_disasm: arg.will_plus_ws2_no_disasm, + #[cfg(feature = "artemis")] + artemis_txt_blacklist_names: std::sync::Arc::new(std::collections::HashSet::from_iter( + arg.artemis_txt_blacklist_names.iter().cloned(), + )), + #[cfg(feature = "artemis")] + artemis_txt_lang: arg.artemis_txt_lang.clone(), }; match &arg.command { args::Command::Export { input, output } => { diff --git a/src/scripts/artemis/mod.rs b/src/scripts/artemis/mod.rs index 579fa1e..b6dd4ed 100644 --- a/src/scripts/artemis/mod.rs +++ b/src/scripts/artemis/mod.rs @@ -3,3 +3,4 @@ pub mod archive; pub mod asb; pub mod ast; +pub mod txt; diff --git a/src/scripts/artemis/txt.rs b/src/scripts/artemis/txt.rs new file mode 100644 index 0000000..8998aeb --- /dev/null +++ b/src/scripts/artemis/txt.rs @@ -0,0 +1,1216 @@ +use crate::scripts::base::*; +use crate::types::*; +use crate::utils::encoding::*; +use crate::utils::escape::*; +use anyhow::Result; +use std::collections::HashSet; +use std::ops::{Deref, DerefMut}; +use std::sync::Arc; +use unicode_segmentation::UnicodeSegmentation; + +#[derive(Debug, Clone)] +/// Artemis TXT script builder +pub struct TxtBuilder {} + +impl TxtBuilder { + /// Creates a new instance of `TxtBuilder` + pub const fn new() -> Self { + Self {} + } +} + +impl ScriptBuilder for TxtBuilder { + fn default_encoding(&self) -> Encoding { + Encoding::Utf8 + } + + fn build_script( + &self, + buf: Vec, + _filename: &str, + encoding: Encoding, + _archive_encoding: Encoding, + config: &ExtraConfig, + _archive: Option<&Box>, + ) -> Result> { + Ok(Box::new(TxtScript::new(buf, encoding, config)?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &["txt"] + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::ArtemisTxt + } +} + +/// Artemis TXT script nodes +pub trait Node { + /// Serialize the node to a string representation. + fn serialize(&self) -> String; +} + +#[derive(Debug, Clone, PartialEq)] +/// Represents a comment node in Artemis TXT scripts. +pub struct CommentNode(pub String); + +impl Node for CommentNode { + fn serialize(&self) -> String { + format!("//{}", self.0) + } +} + +#[derive(Clone, Debug, PartialEq)] +/// Empty Line Node +pub struct EmptyLineNode; + +impl Node for EmptyLineNode { + fn serialize(&self) -> String { + String::new() + } +} + +#[derive(Debug, Clone, PartialEq)] +/// Represents a label node in Artemis TXT scripts. +pub struct LabelNode(pub String); + +impl Node for LabelNode { + fn serialize(&self) -> String { + format!("*{}", self.0) + } +} + +#[derive(Debug, Clone, PartialEq)] +/// Represents a tag node in Artemis TXT scripts. +pub struct TagNode { + /// The name of the tag. + pub name: String, + /// The attributes of the tag, represented as a vector of key-value pairs. + pub attributes: Vec<(String, Option)>, +} + +impl Node for TagNode { + fn serialize(&self) -> String { + let attributes = self + .attributes + .iter() + .map(|(key, value)| { + if let Some(val) = value { + format!("{}=\"{}\"", key, val) + } else { + key.clone() + } + }) + .collect::>() + .join(" "); + if attributes.is_empty() { + format!("[{}]", self.name) + } else { + format!("[{} {}]", self.name, attributes) + } + } +} + +impl TagNode { + fn ser_attributes_xml(&self) -> String { + let mut parts = Vec::new(); + for (key, value) in self.attributes.iter() { + match value { + None => { + parts.push(key.clone()); + } + Some(val) => { + parts.push(format!("{}=\"{}\"", key, escape_xml_attr_value(val))); + } + } + } + parts.join(" ") + } + + /// Get attribute value of attribute in tag by name. + pub fn get_attr(&self, attr: &str) -> Option<&str> { + self.attributes + .iter() + .find(|(key, _)| key == attr) + .and_then(|(_, value)| value.as_deref()) + } + + /// Returns true if the tag is not suitable for name. + pub fn is_blocked_name(&self, set: &HashSet) -> bool { + set.contains(&self.name) + } + + /// Checks if the tag has a specific attribute. + pub fn has_attr(&self, attr: &str) -> bool { + self.attributes.iter().any(|(key, _)| key == attr) + } + + /// Sets the value of an attribute in the tag. + pub fn set_attr(&mut self, attr: &str, value: Option) { + if let Some(pos) = self.attributes.iter().position(|(key, _)| key == attr) { + self.attributes[pos].1 = value; + } else { + self.attributes.push((attr.to_string(), value)); + } + } + + /// Converts the node to a xml-like string representation. + pub fn to_xml(&self) -> String { + let attributes = self.ser_attributes_xml(); + if attributes.is_empty() { + format!("<{}>", self.name) + } else { + format!("<{} {}>", self.name, attributes) + } + } +} + +#[derive(Debug, Clone, PartialEq)] +/// Represents a text node in Artemis TXT scripts. +pub struct TextNode(pub String); + +#[derive(Debug, Clone, PartialEq)] +/// Represents a node in a TXT line. +pub enum TxtLineNode { + Comment(CommentNode), + Tag(TagNode), + Text(TextNode), +} + +impl TxtLineNode { + /// Checks if the node is a tag. + /// + /// * `tag` - The name of the tag. + pub fn is_tag(&self, tag: &str) -> bool { + matches!(self, TxtLineNode::Tag(node) if node.name == tag) + } + + /// Returns true if the tag is a blocked name. + pub fn is_tag_blocked_name(&self, set: &HashSet) -> bool { + if let TxtLineNode::Tag(node) = self { + node.is_blocked_name(set) + } else { + false + } + } + + /// Returns an iterator over the keys of the attributes of the tag node. + pub fn tag_attr_keys<'a>(&'a self) -> Box + 'a> { + if let TxtLineNode::Tag(node) = self { + Box::new(node.attributes.iter().map(|(key, _)| key.as_str())) + } else { + Box::new(std::iter::empty()) + } + } + + pub fn tag_get_attr<'a>(&'a self, attr: &str) -> Option<&'a str> { + if let TxtLineNode::Tag(node) = self { + node.get_attr(attr) + } else { + None + } + } + + /// Checks if the tag has a specific attribute. + pub fn tag_has_attr(&self, attr: &str) -> bool { + if let TxtLineNode::Tag(node) = self { + node.attributes.iter().any(|(key, _)| key == attr) + } else { + false + } + } + + pub fn tag_set_attr(&mut self, attr: &str, value: Option) { + if let TxtLineNode::Tag(node) = self { + node.set_attr(attr, value); + } + } + + /// Converts the node to a xml-like string representation. + pub fn to_xml(&self) -> String { + match self { + TxtLineNode::Comment(_) => String::new(), // Ignore comments in XML + TxtLineNode::Tag(n) => { + if (n.name == "rt2" || n.name == "ret2") && n.attributes.is_empty() { + "\n".to_string() + } else { + n.to_xml() + } + } + TxtLineNode::Text(n) => escape_xml_text_value(&n.0), + } + } +} + +impl Node for TxtLineNode { + fn serialize(&self) -> String { + match self { + TxtLineNode::Comment(node) => node.serialize(), + TxtLineNode::Tag(node) => node.serialize(), + TxtLineNode::Text(node) => node.0.clone(), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +/// Represents a line in Artemis TXT scripts, which can contain multiple nodes. +pub struct TxtLine(pub Vec); + +impl Deref for TxtLine { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for TxtLine { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Node for TxtLine { + fn serialize(&self) -> String { + self.0 + .iter() + .map(|node| node.serialize()) + .collect::>() + .join("") + } +} + +impl TxtLine { + /// Converts the line to a xml-like string representation. + pub fn to_xml(&self) -> String { + self.0 + .iter() + .map(|node| node.to_xml()) + .collect::>() + .join("") + } +} + +#[derive(Debug, Clone, PartialEq)] +/// Represents a parsed line in Artemis TXT scripts. +pub enum ParsedLine { + /// Empty line. + Empty(EmptyLineNode), + /// Comment line. + Comment(CommentNode), + /// Label line. + Label(LabelNode), + /// Line + Line(TxtLine), +} + +impl Node for ParsedLine { + fn serialize(&self) -> String { + match self { + ParsedLine::Empty(node) => node.serialize(), + ParsedLine::Comment(node) => node.serialize(), + ParsedLine::Label(node) => node.serialize(), + ParsedLine::Line(line) => line.serialize(), + } + } +} + +impl ParsedLine { + /// Returns the length of the line. + pub fn len(&self) -> usize { + match self { + ParsedLine::Empty(_) => 0, + ParsedLine::Comment(_) => 0, + ParsedLine::Label(_) => 0, + ParsedLine::Line(line) => line.len(), + } + } + + /// Push a node to the line. + pub fn push(&mut self, node: TxtLineNode) { + if let ParsedLine::Line(line) = self { + line.push(node); + } else { + // Do'nt care about other types + } + } + + /// Inserts a node at the specified index in the line. + pub fn insert(&mut self, index: usize, node: TxtLineNode) { + if let ParsedLine::Line(line) = self { + line.insert(index, node); + } else { + // Do'nt care about other types + } + } + + /// Remove a node at the specified index from the line. + pub fn remove(&mut self, index: usize) -> Option { + if let ParsedLine::Line(line) = self { + if index < line.len() { + Some(line.remove(index)) + } else { + None + } + } else { + // Do'nt care about other types + None + } + } +} + +#[derive(Debug, Clone)] +/// Represents a parsed Artemis TXT script. +pub struct ParsedScript(pub Vec); + +impl Deref for ParsedScript { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ParsedScript { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Node for ParsedScript { + fn serialize(&self) -> String { + self.0 + .iter() + .map(|line| line.serialize()) + .collect::>() + .join("\n") + } +} + +/// Parser for Artemis TXT scripts. +pub struct Parser { + lines: Vec, +} + +impl Parser { + pub fn new + ?Sized>(script: &S) -> Self { + let lines = script.as_ref().lines().map(|s| s.to_string()).collect(); + Self { lines } + } + + pub fn parse(&self, preserve_empty_lines: bool) -> Result { + let mut parsed_script = Vec::new(); + let mut i = 0; + let line_count = self.lines.len(); + while i < line_count { + let line = self.lines[i].trim(); + i += 1; + if line.is_empty() { + if preserve_empty_lines { + parsed_script.push(ParsedLine::Empty(EmptyLineNode)); + } + continue; + } + if line.starts_with("//") { + parsed_script.push(ParsedLine::Comment(CommentNode(line[2..].to_string()))); + continue; + } + if line.starts_with("*") { + let label = line[1..].trim().to_string(); + parsed_script.push(ParsedLine::Label(LabelNode(label))); + continue; + } + let mut temp = String::new(); + let mut nodes = Vec::new(); + let mut line_graphs = line.graphemes(true).collect::>(); + let mut line_pos = 0; + let mut is_comment = false; + while line_pos < line_graphs.len() { + let graph = line_graphs[line_pos]; + line_pos += 1; + temp.push_str(graph); + if is_comment { + continue; + } + if !is_comment && temp.ends_with("//") && temp.len() > 2 { + nodes.push(TxtLineNode::Text(TextNode( + temp[..temp.len() - 2].to_string(), + ))); + temp.clear(); + is_comment = true; + continue; + } + if graph == "[" { + if !temp.trim_end_matches("[").is_empty() { + nodes.push(TxtLineNode::Text(TextNode( + temp.trim_end_matches("[").to_string(), + ))); + } + // Tag may end in another line, so we need check it. + while !line_graphs[line_pos..].contains(&"]") { + if i < line_count { + let nline = self.lines[i].trim(); + i += 1; + // Add next line to the current line + line_graphs.push("\n"); + line_graphs.extend(nline.graphemes(true)); + } else { + break; + } + } + let (tag, nextpos) = TagParser { + graphs: &line_graphs, + pos: line_pos, + } + .parse()?; + line_pos = nextpos; + nodes.push(TxtLineNode::Tag(tag)); + temp.clear(); + continue; + } + } + if is_comment { + nodes.push(TxtLineNode::Comment(CommentNode(temp))); + } else { + if !temp.is_empty() { + nodes.push(TxtLineNode::Text(TextNode(temp))); + } + } + parsed_script.push(ParsedLine::Line(TxtLine(nodes))); + } + Ok(ParsedScript(parsed_script)) + } +} + +struct TagParser<'a> { + graphs: &'a [&'a str], + pos: usize, +} + +impl<'a> TagParser<'a> { + fn peek(&self) -> Option<&'a str> { + self.graphs.get(self.pos).cloned() + } + + fn eat(&mut self) { + if self.pos < self.graphs.len() { + self.pos += 1; + } + } + + fn next(&mut self) -> Option<&'a str> { + if self.pos < self.graphs.len() { + let graph = self.graphs[self.pos]; + self.pos += 1; + Some(graph) + } else { + None + } + } + + fn is_indent(&self, indent: &str) -> bool { + let mut pos = self.pos; + for ident in indent.graphemes(true) { + if pos >= self.graphs.len() || self.graphs[pos] != ident { + return false; + } + pos += 1; + } + true + } + + fn eat_all_equal(&mut self) { + while let Some(graph) = self.peek() { + if graph == "=" { + self.eat(); + } else { + break; + } + } + } + + fn parse(&mut self) -> Result<(TagNode, usize)> { + let name = self.parse_tag()?; + self.erase_whitespace(); + let mut attributes = Vec::new(); + loop { + let graph = match self.peek() { + Some(g) => g, + None => { + return Err(anyhow::anyhow!( + "Unexpected end of tag parsing: {}", + self.graphs.join("") + )); + } + }; + if graph == "]" { + self.eat(); + break; + } + if graph == " " || graph == "\t" { + self.eat(); + continue; + } + if graph == "=" { + return Err(anyhow::anyhow!("Unexpected '=' without attribute name")); + } + let attr_name = self.parse_attr_name()?; + self.erase_whitespace(); + let graph = match self.peek() { + Some(g) => g, + None => { + return Err(anyhow::anyhow!( + "Unexpected end of tag parsing: {}", + self.graphs.join("") + )); + } + }; + if graph == "]" { + self.eat(); + attributes.push((attr_name, None)); + break; + } + if graph == "=" { + // Sometimes the script contains multiple equal signs + // We just ignore them + // Example: [イベントCG st = "拡大/ev005/a" add = "拡大/ev005/y2,拡大/ev005/r5" left = "min ~ max" top = "max ~ 1/4" mtime = "60000" ease == "減速" mfade = "1000" hide = "1"] + self.eat_all_equal(); + self.erase_whitespace(); + let value = self.parse_attr_value()?; + attributes.push((attr_name, Some(value))); + self.erase_whitespace(); + } else { + attributes.push((attr_name, None)); + self.erase_whitespace(); + continue; + } + } + return Ok((TagNode { name, attributes }, self.pos)); + } + + fn erase_whitespace(&mut self) { + while let Some(graph) = self.peek() { + if graph == " " || graph == "\t" { + self.eat(); + } else { + break; + } + } + } + + fn parse_attr_name(&mut self) -> Result { + let mut attr_name = String::new(); + while let Some(graph) = self.peek() { + if graph == "=" || graph == " " || graph == "\t" || graph == "]" { + break; + } + attr_name.push_str(graph); + self.eat(); + } + if attr_name.is_empty() { + return Err(anyhow::anyhow!("Empty attribute name found")); + } + Ok(attr_name) + } + + fn parse_attr_value(&mut self) -> Result { + let mut value = String::new(); + if !self.is_indent("\"") { + return Err(anyhow::anyhow!( + "Expected attribute value to start with a quote: {}", + self.graphs.join("") + )); + } + self.eat(); // Skip the opening quote + while let Some(graph) = self.next() { + if graph == "\"" { + break; // End of attribute value + } + value.push_str(graph); + } + Ok(value) + } + + fn parse_tag(&mut self) -> Result { + let mut tag = String::new(); + while let Some(graph) = self.peek() { + if graph == " " || graph == "\t" || graph == "]" { + break; + } + tag.push_str(graph); + self.eat(); + } + if tag.is_empty() { + return Err(anyhow::anyhow!("Empty tag found")); + } + Ok(tag) + } +} + +struct XMLTextParser<'a> { + str: &'a str, + lang: &'a str, + pos: usize, +} + +impl<'a> XMLTextParser<'a> { + pub fn new(text: &'a str, lang: &'a str) -> Self { + Self { + str: text, + lang, + pos: 0, + } + } + + fn parse_tag(&mut self) -> Result { + let mut name = String::new(); + let mut attributes = Vec::new(); + let mut is_name = true; + let mut is_key = false; + let mut is_value = false; + let mut is_in_quote = false; + let mut key = String::new(); + let mut value = String::new(); + while let Some(c) = self.next() { + match c { + '>' => { + if !name.is_empty() { + return Ok(TagNode { name, attributes }); + } else { + return Err(anyhow::anyhow!("Empty tag name")); + } + } + ' ' | '\t' => { + if is_name { + is_name = false; + is_key = true; + } else if is_key { + if !key.is_empty() { + attributes.push((key.clone(), None)); + key.clear(); + } + } else if is_value { + if is_in_quote { + value.push(c); + } else { + if !value.is_empty() { + attributes.push((key.clone(), Some(unescape_xml(&value)))); + key.clear(); + value.clear(); + } + is_key = true; + is_value = false; + } + } + } + '"' => { + if is_in_quote { + is_in_quote = false; + if !value.is_empty() { + attributes.push((key.clone(), Some(unescape_xml(&value)))); + key.clear(); + value.clear(); + } + is_key = true; + } else { + is_in_quote = true; + } + } + '=' => { + if is_key { + is_key = false; + is_value = true; + } + } + _ => { + if is_name { + name.push(c); + } else if is_key { + key.push(c); + } else if is_value { + value.push(c); + } else { + return Err(anyhow::anyhow!("Unexpected character in tag: {}", c)); + } + } + } + } + Err(anyhow::anyhow!("Unexpected end of input while parsing tag")) + } + + pub fn parse(mut self) -> Result> { + let mut lines = Vec::new(); + let mut current_line = Vec::new(); + let mut text = String::new(); + current_line.push(TxtLineNode::Tag(TagNode { + name: "lang".to_string(), + attributes: vec![(self.lang.to_string(), None)], + })); + while let Some(c) = self.next() { + match c { + '<' => { + if !text.is_empty() { + current_line.push(TxtLineNode::Text(TextNode(unescape_xml(&text)))); + text.clear(); + } + let tag = self.parse_tag()?; + let is_r = tag.name == "rt2" || tag.name == "ret2"; + current_line.push(TxtLineNode::Tag(tag)); + if is_r { + lines.push(ParsedLine::Line(TxtLine(current_line))); + current_line = Vec::new(); + } + } + '\n' => { + if !text.is_empty() { + current_line.push(TxtLineNode::Text(TextNode(unescape_xml(&text)))); + text.clear(); + } + current_line.push(TxtLineNode::Tag(TagNode { + name: "rt2".to_string(), + attributes: Vec::new(), + })); + lines.push(ParsedLine::Line(TxtLine(current_line))); + current_line = Vec::new(); + } + _ => text.push(c), + } + } + if !text.is_empty() { + current_line.push(TxtLineNode::Text(TextNode(unescape_xml(&text)))); + } + current_line.push(TxtLineNode::Tag(TagNode { + name: "/lang".to_string(), + attributes: Vec::new(), + })); + lines.push(ParsedLine::Line(TxtLine(current_line))); + Ok(lines) + } + + fn next(&mut self) -> Option { + if self.pos < self.str.len() { + let c = self.str[self.pos..].chars().next()?; + self.pos += c.len_utf8(); + Some(c) + } else { + None + } + } +} + +#[derive(Debug)] +pub struct TxtScript { + tree: ParsedScript, + blacklist_names: Arc>, + lang: Option, +} + +impl TxtScript { + /// Creates a new instance of `TxtScript` from the given buffer and encoding. + pub fn new(buf: Vec, encoding: Encoding, config: &ExtraConfig) -> Result { + let script = decode_to_string(encoding, &buf, true)?; + let parser = Parser::new(&script); + let tree = parser.parse(true)?; + Ok(Self { + tree, + blacklist_names: config.artemis_txt_blacklist_names.clone(), + lang: config.artemis_txt_lang.clone(), + }) + } +} + +impl Script for TxtScript { + 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(); + let mut i = 0; + let len = self.tree.len(); + let mut last_tag_block: Option = None; + let mut lang = self.lang.as_ref().map(|s| s.as_str()); + let mut message = TxtLine(Vec::new()); + let mut in_lang_block = false; + let mut droped_lang_block = false; + let mut is_selectblk = false; + while i < len { + let line = &self.tree[i]; + if let ParsedLine::Line(line) = line { + for node in line.iter() { + if node.is_tag("lang") { + let lan = match lang { + Some(l) => l, + None => { + for key in node.tag_attr_keys() { + lang = Some(key); + break; + } + match lang { + Some(l) => l, + None => { + return Err(anyhow::anyhow!( + "No language found in lang tag" + )); + } + } + } + }; + if node.tag_has_attr(lan) { + in_lang_block = true; + } else { + droped_lang_block = true; + } + } else if node.is_tag("/lang") { + in_lang_block = false; + droped_lang_block = false; + } else if node.is_tag("printlang") { + let mes = message.to_xml(); + message.clear(); + if !mes.is_empty() { + let name = if mes.starts_with("「") { + match &last_tag_block { + Some(block) => { + Some(if let Some(name) = block.get_attr("name") { + name.to_string() + } else { + block.name.clone() + }) + } + _ => { + eprintln!("Warn: Name block not found."); + crate::COUNTER.inc_warning(); + None + } + } + } else { + None + }; + messages.push(Message { name, message: mes }); + } + last_tag_block = None; + } else if node.is_tag("selectbtn_init") { + is_selectblk = true; + } else if node.is_tag("selectbtn") { + let mut lan = match lang { + Some(l) => l, + None => { + for key in node.tag_attr_keys() { + if key == "label" || key == "call" { + continue; + } + lang = Some(key); + break; + } + match lang { + Some(l) => l, + None => { + return Err(anyhow::anyhow!( + "No language found in selectbtn tag" + )); + } + } + } + }; + if !node.tag_has_attr(lan) { + for key in node.tag_attr_keys() { + if key == "label" || key == "call" { + continue; + } + lan = key; + break; + } + } + if let Some(t) = node.tag_get_attr(lan) { + messages.push(Message { + name: None, + message: t.to_string(), + }); + } + } else if node.is_tag("/selectbtn") { + is_selectblk = false; + } else if in_lang_block { + message.push(node.clone()); + } else if droped_lang_block { + // Drop the message if we are in a dropped lang block + } else if is_selectblk { + // Drop other nodes in select block + } else if let TxtLineNode::Tag(tag) = node { + if !tag.is_blocked_name(&self.blacklist_names) { + last_tag_block = Some(tag.clone()); + } + } + } + } + i += 1; + } + Ok(messages) + } + + fn import_messages<'a>( + &'a self, + messages: Vec, + mut file: Box, + _filename: &str, + encoding: Encoding, + replacement: Option<&'a ReplacementTable>, + ) -> Result<()> { + let mut output = self.tree.clone(); + let mut current_line = 0; + let mut last_tag_block_loc = None; + let mut lang = self.lang.clone(); + let mut mes = messages.iter(); + let mut mess = mes.next(); + let mut lang_block_index = None; + let mut lang_end_block_index = None; + let mut in_lang_block = false; + let mut droped_lang_block = false; + let mut is_selectblk = false; + while current_line < output.len() { + let line = output[current_line].clone(); + if let ParsedLine::Line(line) = &line { + for (i, node) in line.iter().enumerate() { + if node.is_tag("lang") { + let lan = match lang.as_ref() { + Some(l) => l.as_str(), + None => { + for key in node.tag_attr_keys() { + lang = Some(key.to_string()); + break; + } + match lang.as_ref() { + Some(l) => l.as_str(), + None => { + return Err(anyhow::anyhow!( + "No language found in lang tag" + )); + } + } + } + }; + if node.tag_has_attr(lan) { + in_lang_block = true; + lang_block_index = Some((current_line, i)); + } else { + droped_lang_block = true; + } + } else if node.is_tag("/lang") { + if in_lang_block { + lang_end_block_index = Some((current_line, i)); + } + in_lang_block = false; + droped_lang_block = false; + } else if node.is_tag("printlang") { + let lan = lang + .as_ref() + .map(|s| s.as_str()) + .ok_or(anyhow::anyhow!("No language specified."))?; + let m = match mess { + Some(m) => m, + None => { + return Err(anyhow::anyhow!("Not enough messages.")); + } + }; + if let Some(name) = &m.name { + let block_index: (usize, usize) = match last_tag_block_loc.take() { + Some(data) => data, + None => { + return Err(anyhow::anyhow!( + "No name tag block found before printlang.", + )); + } + }; + let mut name = name.clone(); + if let Some(repl) = replacement { + for (k, v) in &repl.map { + name = name.replace(k, v); + } + } + let mblock = &mut output[block_index.0]; + if let ParsedLine::Line(txt_line) = mblock { + let block = txt_line[block_index.1].clone(); + if let TxtLineNode::Tag(mut tag) = block { + tag.set_attr("name", Some(name)); + txt_line[block_index.1] = TxtLineNode::Tag(tag); + } else { + return Err(anyhow::anyhow!( + "Last tag block is not a tag: {:?}", + mblock + )); + } + } else { + return Err(anyhow::anyhow!( + "Last tag block is not a line: {:?}", + mblock + )); + } + } + let mut message = m.message.clone(); + if let Some(repl) = replacement { + for (k, v) in &repl.map { + message = message.replace(k, v); + } + } + let mut nodes = XMLTextParser::new(&message, lan).parse()?; + if lang_block_index.is_some() && lang_end_block_index.is_some() { + let start_index = lang_block_index.unwrap(); + let end_index = lang_end_block_index.unwrap(); + if start_index.1 != 0 { + let block = output[start_index.0].clone(); + if let ParsedLine::Line(txt_line) = block { + for i in 0..start_index.1 { + nodes[0].insert(i, txt_line[i].clone()); + } + } else { + return Err(anyhow::anyhow!( + "Lang block start is not a line: {:?}", + block + )); + } + } + if end_index.1 + 1 < output[end_index.0].len() { + let block = output[end_index.0].clone(); + if let ParsedLine::Line(txt_line) = block { + for i in end_index.1 + 1..txt_line.len() { + nodes.last_mut().unwrap().push(txt_line[i].clone()); + } + } else { + return Err(anyhow::anyhow!( + "Lang block end is not a line: {:?}", + block + )); + } + } + let ori_len = (end_index.0 - start_index.0 + 1) as isize; + let new_len = nodes.len() as isize; + for _ in start_index.0..=end_index.0 { + output.remove(start_index.0); + } + let mut start_index = start_index.0; + for node in nodes { + output.insert(start_index, node); + start_index += 1; + } + current_line = (current_line as isize + new_len - ori_len) as usize; + } else { + // Add a new lang block if not exists + for node in nodes { + output.insert(current_line, node); + current_line += 1; + } + } + lang_block_index = None; + lang_end_block_index = None; + mess = mes.next(); + last_tag_block_loc = None; + } else if node.is_tag("selectbtn_init") { + is_selectblk = true; + } else if node.is_tag("selectbtn") { + let lan = match lang.as_ref() { + Some(l) => l.as_str(), + None => { + for key in node.tag_attr_keys() { + if key == "label" || key == "call" { + continue; + } + lang = Some(key.to_string()); + break; + } + match lang.as_ref() { + Some(l) => l.as_str(), + None => { + return Err(anyhow::anyhow!( + "No language found in selectbtn tag" + )); + } + } + } + }; + let m = match mess { + Some(m) => m, + None => { + return Err(anyhow::anyhow!("Not enough messages.")); + } + }; + let mut message = m.message.clone(); + if let Some(repl) = replacement { + for (k, v) in &repl.map { + message = message.replace(k, v); + } + } + let mut node = node.clone(); + node.tag_set_attr(lan, Some(message)); + let block = &mut output[current_line]; + if let ParsedLine::Line(txt_line) = block { + txt_line[i] = node; + } else { + return Err(anyhow::anyhow!("selectbtn tag not in line: {:?}", block)); + } + mess = mes.next(); + } else if node.is_tag("/selectbtn") { + is_selectblk = false; + } else if in_lang_block { + // Do nothing + } else if droped_lang_block { + // Drop the message if we are in a dropped lang block + } else if is_selectblk { + // Ignore other nodes in select block + } else if let TxtLineNode::Tag(tag) = node { + if !tag.is_blocked_name(&self.blacklist_names) { + last_tag_block_loc = Some((current_line, i)); + } + } + } + } + current_line += 1; + } + let s = output.serialize(); + let encoded = encode_string(encoding, &s, false)?; + file.write_all(&encoded)?; + file.flush()?; + Ok(()) + } +} + +#[test] +fn test_xml_parser() { + let data = "测试文本\nok测试"; + let data = XMLTextParser::new(data, "en").parse().unwrap(); + assert_eq!( + data, + vec![ + ParsedLine::Line(TxtLine(vec![ + TxtLineNode::Tag(TagNode { + name: "lang".to_string(), + attributes: vec![("en".to_string(), None)], + }), + TxtLineNode::Text(TextNode("测试文本".to_string())), + TxtLineNode::Tag(TagNode { + name: "rt2".to_string(), + attributes: vec![], + }), + ])), + ParsedLine::Line(TxtLine(vec![ + TxtLineNode::Text(TextNode("ok".to_string())), + TxtLineNode::Tag(TagNode { + name: "r".to_string(), + attributes: vec![("a".to_string(), Some("b".to_string()))], + }), + TxtLineNode::Text(TextNode("测试".to_string())), + TxtLineNode::Tag(TagNode { + name: "b".to_string(), + attributes: vec![("o".to_string(), Some("文本\n换行".to_string()))], + }), + TxtLineNode::Tag(TagNode { + name: "/lang".to_string(), + attributes: vec![], + }), + ])), + ], + ); +} diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index 74ebac9..cafa589 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -112,6 +112,8 @@ lazy_static::lazy_static! { Box::new(entis_gls::srcxml::SrcXmlScriptBuilder::new()), #[cfg(feature = "softpal")] Box::new(softpal::scr::SoftpalScriptBuilder::new()), + #[cfg(feature = "artemis")] + Box::new(artemis::txt::TxtBuilder::new()), ]; /// A list of all script extensions. pub static ref ALL_EXTS: Vec = diff --git a/src/types.rs b/src/types.rs index 11d7e65..afaf7fe 100644 --- a/src/types.rs +++ b/src/types.rs @@ -344,6 +344,14 @@ pub struct ExtraConfig { /// Use another parser to parse the script. /// Should only be used when the default parser not works well. pub will_plus_ws2_no_disasm: bool, + #[cfg(feature = "artemis")] + /// Artemis Engine blacklist tag names for TXT script. + /// This is used to ignore these tags when finding names in Artemis TXT script. + pub artemis_txt_blacklist_names: std::sync::Arc>, + #[cfg(feature = "artemis")] + /// Specify the language of Artemis TXT script. + /// If not specified, the first language will be used. + pub artemis_txt_lang: Option, } #[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord)] @@ -355,6 +363,9 @@ pub enum ScriptType { #[cfg(feature = "artemis")] /// Artemis Engine ASB script ArtemisAsb, + #[cfg(feature = "artemis")] + /// Artemis Engine TXT script + ArtemisTxt, #[cfg(feature = "artemis-arc")] #[value(alias("pfs"))] /// Artemis archive (pfs)