msg_tool\scripts\artemis/
txt.rs1use crate::scripts::base::*;
2use crate::types::*;
3use crate::utils::encoding::*;
4use anyhow::{Result, anyhow};
5use std::io::Write;
6
7#[derive(Debug)]
8pub struct ArtemisTxtBuilder {}
10
11impl ArtemisTxtBuilder {
12 pub const fn new() -> Self {
14 Self {}
15 }
16}
17
18impl ScriptBuilder for ArtemisTxtBuilder {
19 fn default_encoding(&self) -> Encoding {
20 Encoding::Cp932
21 }
22
23 fn build_script(
24 &self,
25 buf: Vec<u8>,
26 _filename: &str,
27 encoding: Encoding,
28 _archive_encoding: Encoding,
29 _config: &ExtraConfig,
30 _archive: Option<&Box<dyn Script>>,
31 ) -> Result<Box<dyn Script>> {
32 Ok(Box::new(ArtemisTxtScript::new(buf, encoding)?))
33 }
34
35 fn extensions(&self) -> &'static [&'static str] {
36 &["txt"]
37 }
38
39 fn script_type(&self) -> &'static ScriptType {
40 &ScriptType::ArtemisTxt
41 }
42}
43
44#[derive(Debug, Clone)]
45struct MessageRef {
46 line_index: usize,
47 speaker: Option<String>,
48 speaker_line_index: Option<usize>,
49}
50
51#[derive(Debug)]
52pub struct ArtemisTxtScript {
53 lines: Vec<String>,
54 message_map: Vec<MessageRef>,
55 use_crlf: bool,
56 trailing_newline: bool,
57}
58
59impl ArtemisTxtScript {
60 fn new(buf: Vec<u8>, encoding: Encoding) -> Result<Self> {
61 let script = decode_to_string(encoding, &buf, true)?;
62 let use_crlf = script.contains("\r\n");
63 let trailing_newline = script.ends_with('\n');
64 let mut lines: Vec<String> = script
65 .split('\n')
66 .map(|line| {
67 if use_crlf {
68 line.strip_suffix('\r').unwrap_or(line).to_string()
69 } else {
70 line.to_string()
71 }
72 })
73 .collect();
74 if trailing_newline {
75 if lines.last().map(|s| s.is_empty()).unwrap_or(false) {
77 lines.pop();
78 }
79 }
80 let message_map = Self::collect_messages(&lines);
81 Ok(Self {
82 lines,
83 message_map,
84 use_crlf,
85 trailing_newline,
86 })
87 }
88
89 fn collect_messages(lines: &[String]) -> Vec<MessageRef> {
90 let mut refs = Vec::new();
91 let mut current_speaker: Option<String> = None;
92 let mut current_speaker_line: Option<usize> = None;
93 for (idx, line) in lines.iter().enumerate() {
94 let trimmed = line.trim();
95 if trimmed.is_empty() {
96 continue;
97 }
98 if trimmed.starts_with("//") {
99 continue;
100 }
101 if trimmed.starts_with('*') {
102 continue;
103 }
104 if trimmed.starts_with('[') {
105 continue;
106 }
107 if trimmed.starts_with('#') {
108 match Self::parse_hash_speaker(trimmed) {
109 Some(name) => {
110 current_speaker = Some(name);
111 current_speaker_line = Some(idx);
112 }
113 None => {
114 current_speaker = None;
115 current_speaker_line = None;
116 }
117 }
118 continue;
119 }
120
121 let speaker = if Self::is_dialogue_line(trimmed) {
122 current_speaker.clone()
123 } else {
124 None
125 };
126 let speaker_line_index = if speaker.is_some() {
127 current_speaker_line
128 } else {
129 None
130 };
131 refs.push(MessageRef {
132 line_index: idx,
133 speaker,
134 speaker_line_index,
135 });
136 }
137 refs
138 }
139
140 fn parse_hash_speaker(line: &str) -> Option<String> {
141 let content = line.trim_start_matches('#').trim();
142 if content.is_empty() {
143 return None;
144 }
145 let mut parts = content.split_whitespace();
146 let token = parts.next()?;
147 let upper = token.to_ascii_uppercase();
148 if upper.starts_with("BGM")
149 || upper.starts_with("SE")
150 || upper.starts_with("FGA")
151 || upper.starts_with("FG")
152 {
153 return None;
154 }
155 if token == "服装" {
156 return None;
157 }
158 Some(token.to_string())
159 }
160
161 fn is_dialogue_line(line: &str) -> bool {
162 match line.chars().next() {
163 Some('"') | Some('“') | Some('〝') | Some('(') | Some('(') | Some('「')
164 | Some('『') => true,
165 _ => false,
166 }
167 }
168
169 fn join_lines(&self, lines: &[String]) -> String {
170 let newline = if self.use_crlf { "\r\n" } else { "\n" };
171 let mut combined = lines.join(newline);
172 if self.trailing_newline {
173 combined.push_str(newline);
174 }
175 combined
176 }
177
178 fn set_speaker_line(line: &str, name: &str) -> String {
179 if let Some(hash_pos) = line.find('#') {
180 let after_hash = &line[hash_pos + 1..];
181 let start_rel = after_hash
182 .char_indices()
183 .find(|(_, ch)| !ch.is_whitespace())
184 .map(|(offset, _)| offset);
185 let start_rel = match start_rel {
186 Some(offset) => offset,
187 None => {
188 let mut result = String::with_capacity(line.len() + name.len());
189 result.push_str(line);
190 result.push_str(name);
191 return result;
192 }
193 };
194 let start = hash_pos + 1 + start_rel;
195 let tail = &after_hash[start_rel..];
196 let mut name_len = 0;
197 let mut end_rel = tail.len();
198 for (offset, ch) in tail.char_indices() {
199 if ch.is_whitespace() {
200 end_rel = offset;
201 break;
202 }
203 name_len = offset + ch.len_utf8();
204 }
205 let end = if tail.is_empty() {
206 start
207 } else if end_rel == tail.len() {
208 start + name_len
209 } else {
210 start + end_rel
211 };
212 let mut result = String::with_capacity(line.len() + name.len());
213 result.push_str(&line[..start]);
214 result.push_str(name);
215 result.push_str(&line[end..]);
216 return result;
217 }
218 format!("#{}", name)
219 }
220}
221
222impl Script for ArtemisTxtScript {
223 fn default_output_script_type(&self) -> OutputScriptType {
224 OutputScriptType::Json
225 }
226
227 fn default_format_type(&self) -> FormatOptions {
228 FormatOptions::None
229 }
230
231 fn extract_messages(&self) -> Result<Vec<Message>> {
232 let mut messages = Vec::with_capacity(self.message_map.len());
233 for entry in &self.message_map {
234 let text = self
235 .lines
236 .get(entry.line_index)
237 .cloned()
238 .unwrap_or_default();
239 messages.push(Message {
240 name: entry.speaker.clone(),
241 message: text,
242 });
243 }
244 Ok(messages)
245 }
246
247 fn import_messages<'a>(
248 &'a self,
249 messages: Vec<Message>,
250 mut file: Box<dyn WriteSeek + 'a>,
251 _filename: &str,
252 encoding: Encoding,
253 replacement: Option<&'a ReplacementTable>,
254 ) -> Result<()> {
255 if messages.len() != self.message_map.len() {
256 return Err(anyhow!(
257 "Message count mismatch: expected {}, got {}",
258 self.message_map.len(),
259 messages.len()
260 ));
261 }
262 let mut output_lines = self.lines.clone();
263 for (entry, message) in self.message_map.iter().zip(messages.iter()) {
264 let mut text = message.message.clone();
265 if let Some(repl) = replacement {
266 for (from, to) in &repl.map {
267 text = text.replace(from, to);
268 }
269 }
270 if let Some(line) = output_lines.get_mut(entry.line_index) {
271 *line = text;
272 }
273 if let (Some(speaker_line_index), Some(name)) =
274 (entry.speaker_line_index, message.name.as_ref())
275 {
276 let mut patched_name = name.clone();
277 if let Some(repl) = replacement {
278 for (from, to) in &repl.map {
279 patched_name = patched_name.replace(from, to);
280 }
281 }
282 if let Some(line) = output_lines.get_mut(speaker_line_index) {
283 *line = Self::set_speaker_line(line, &patched_name);
284 } else {
285 return Err(anyhow!(
286 "Speaker line index out of bounds: {}",
287 speaker_line_index
288 ));
289 }
290 }
291 }
292 let combined = self.join_lines(&output_lines);
293 let encoded = encode_string(encoding, &combined, true)?;
294 file.write_all(&encoded)?;
295 Ok(())
296 }
297}