1use super::mdf::Mdf;
3use crate::ext::io::*;
4use crate::ext::psb::*;
5use crate::scripts::base::*;
6use crate::types::*;
7use crate::utils::encoding::*;
8use anyhow::Result;
9use emote_psb::{PsbReader, PsbWriter};
10use std::collections::{HashMap, HashSet};
11use std::io::{Read, Seek};
12use std::path::Path;
13use std::sync::Arc;
14
15#[derive(Debug)]
16pub struct ScnScriptBuilder {}
18
19impl ScnScriptBuilder {
20 pub fn new() -> Self {
22 Self {}
23 }
24}
25
26impl ScriptBuilder for ScnScriptBuilder {
27 fn default_encoding(&self) -> Encoding {
28 Encoding::Utf8
29 }
30
31 fn build_script(
32 &self,
33 buf: Vec<u8>,
34 filename: &str,
35 _encoding: Encoding,
36 _archive_encoding: Encoding,
37 config: &ExtraConfig,
38 _archive: Option<&Box<dyn Script>>,
39 ) -> Result<Box<dyn Script>> {
40 Ok(Box::new(ScnScript::new(
41 MemReader::new(buf),
42 filename,
43 config,
44 )?))
45 }
46
47 fn build_script_from_file(
48 &self,
49 filename: &str,
50 _encoding: Encoding,
51 _archive_encoding: Encoding,
52 config: &ExtraConfig,
53 _archive: Option<&Box<dyn Script>>,
54 ) -> Result<Box<dyn Script>> {
55 if filename == "-" {
56 let data = crate::utils::files::read_file(filename)?;
57 Ok(Box::new(ScnScript::new(
58 MemReader::new(data),
59 filename,
60 config,
61 )?))
62 } else {
63 let f = std::fs::File::open(filename)?;
64 let reader = std::io::BufReader::new(f);
65 Ok(Box::new(ScnScript::new(reader, filename, config)?))
66 }
67 }
68
69 fn build_script_from_reader(
70 &self,
71 reader: Box<dyn ReadSeek>,
72 filename: &str,
73 _encoding: Encoding,
74 _archive_encoding: Encoding,
75 config: &ExtraConfig,
76 _archive: Option<&Box<dyn Script>>,
77 ) -> Result<Box<dyn Script>> {
78 Ok(Box::new(ScnScript::new(reader, filename, config)?))
79 }
80
81 fn extensions(&self) -> &'static [&'static str] {
82 &["ks.scn"]
83 }
84
85 fn script_type(&self) -> &'static ScriptType {
86 &ScriptType::KirikiriScn
87 }
88
89 fn is_this_format(&self, filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
90 if Path::new(filename)
91 .file_name()
92 .map(|name| {
93 name.to_ascii_lowercase()
94 .to_string_lossy()
95 .ends_with(".ks.scn")
96 })
97 .unwrap_or(false)
98 && buf_len >= 4
99 && buf.starts_with(b"PSB\0")
100 {
101 return Some(255);
102 }
103 None
104 }
105}
106
107#[derive(Debug)]
108pub struct ScnScript {
110 psb: VirtualPsbFixed,
111 language_index: usize,
112 export_comumode: bool,
113 filename: String,
114 comumode_json: Option<Arc<HashMap<String, String>>>,
115 custom_yaml: bool,
116}
117
118impl ScnScript {
119 pub fn new<R: Read + Seek>(
125 mut reader: R,
126 filename: &str,
127 config: &ExtraConfig,
128 ) -> Result<Self> {
129 let mut header = [0u8; 4];
130 reader.read_exact(&mut header)?;
131 if &header == b"mdf\0" {
132 let mut data = Vec::new();
133 reader.read_to_end(&mut data)?;
134 let decoded = Mdf::unpack(MemReaderRef::new(&data))?;
135 return Self::new(MemReader::new(decoded), filename, config);
136 }
137 reader.rewind()?;
138 let mut psb = PsbReader::open_psb(reader)
139 .map_err(|e| anyhow::anyhow!("Failed to open PSB from {}: {:?}", filename, e))?;
140 let psb = psb
141 .load()
142 .map_err(|e| anyhow::anyhow!("Failed to load PSB from {}: {:?}", filename, e))?;
143 Ok(Self {
144 psb: psb.to_psb_fixed(),
145 language_index: config.kirikiri_language_index.unwrap_or(0),
146 export_comumode: config.kirikiri_export_comumode,
147 filename: filename.to_string(),
148 comumode_json: config.kirikiri_comumode_json.clone(),
149 custom_yaml: config.custom_yaml,
150 })
151 }
152}
153
154impl Script for ScnScript {
155 fn default_output_script_type(&self) -> OutputScriptType {
156 OutputScriptType::Json
157 }
158
159 fn default_format_type(&self) -> FormatOptions {
160 FormatOptions::None
161 }
162
163 fn is_output_supported(&self, _: OutputScriptType) -> bool {
164 true
165 }
166
167 fn custom_output_extension<'a>(&'a self) -> &'a str {
168 if self.custom_yaml { "yaml" } else { "json" }
169 }
170
171 fn extract_messages(&self) -> Result<Vec<Message>> {
172 let mut messages = Vec::new();
173 let root = self.psb.root();
174 let scenes = root
175 .get_value("scenes")
176 .ok_or(anyhow::anyhow!("scenes not found"))?;
177 let scenes = match scenes {
178 PsbValueFixed::List(list) => list,
179 _ => return Err(anyhow::anyhow!("scenes is not a list")),
180 };
181 let mut comu = if self.export_comumode {
182 Some(ExportComuMes::new())
183 } else {
184 None
185 };
186 for (i, oscene) in scenes.iter().enumerate() {
187 let scene = match oscene {
188 PsbValueFixed::Object(obj) => obj,
189 _ => return Err(anyhow::anyhow!("scene at index {} is not an object", i)),
190 };
191 if let Some(PsbValueFixed::List(texts)) = scene.get_value("texts") {
192 for (j, text) in texts.iter().enumerate() {
193 if let PsbValueFixed::List(text) = text {
194 let values = text.values();
195 if values.len() <= 1 {
196 continue; }
198 let name = &values[0];
199 let name = match name {
200 PsbValueFixed::String(s) => Some(s),
201 PsbValueFixed::Null => None,
202 _ => return Err(anyhow::anyhow!("name is not a string or null")),
203 };
204 let mut display_name;
205 let mut message;
206 if matches!(values[1], PsbValueFixed::List(_)) {
207 display_name = None;
208 message = &values[1];
209 } else {
210 if values.len() <= 2 {
211 continue; }
213 display_name = match &values[1] {
214 PsbValueFixed::String(s) => Some(s),
215 PsbValueFixed::Null => None,
216 _ => {
217 return Err(anyhow::anyhow!(
218 "display name is not a string or null at {i},{j}"
219 ));
220 }
221 };
222 message = &values[2];
223 }
224 if matches!(message, PsbValueFixed::List(_)) {
225 let tmp = message;
226 if let PsbValueFixed::List(list) = tmp {
227 if list.len() > self.language_index {
228 if let PsbValueFixed::List(data) =
229 &list.values()[self.language_index]
230 {
231 if data.len() >= 2 {
232 let data = data.values();
233 display_name = match &data[0] {
234 PsbValueFixed::String(s) => Some(s),
235 PsbValueFixed::Null => None,
236 _ => {
237 return Err(anyhow::anyhow!(
238 "display name is not a string or null at {i},{j}"
239 ));
240 }
241 };
242 message = &data[1];
243 }
244 }
245 }
246 }
247 }
248 if let PsbValueFixed::String(message) = message {
249 match name {
250 Some(name) => {
251 let name = match display_name {
252 Some(name) => name.string(),
253 None => name.string(),
254 };
255 let message = message.string();
256 messages.push(Message {
257 name: Some(name.to_string()),
258 message: message.replace("\\n", "\n"),
259 });
260 }
261 None => {
262 let message = message.string();
263 messages.push(Message {
264 name: None,
265 message: message.replace("\\n", "\n"),
266 });
267 }
268 }
269 }
270 }
271 }
272 }
273 if let Some(PsbValueFixed::List(selects)) = scene.get_value("selects") {
274 for select in selects.iter() {
275 if let PsbValueFixed::Object(select) = select {
276 let mut text = None;
277 if let Some(PsbValueFixed::List(language)) = select.get_value("language") {
278 if language.len() > self.language_index {
279 let v = &language.values()[self.language_index];
280 if let PsbValueFixed::Object(v) = v {
281 text = match v.get_value("text") {
282 Some(PsbValueFixed::String(s)) => Some(s),
283 Some(PsbValueFixed::Null) => None,
284 None => None,
285 _ => {
286 return Err(anyhow::anyhow!(
287 "select text is not a string or null"
288 ));
289 }
290 }
291 }
292 }
293 }
294 if text.is_none() {
295 text = match select.get_value("text") {
296 Some(PsbValueFixed::String(s)) => Some(s),
297 Some(PsbValueFixed::Null) => None,
298 None => None,
299 _ => {
300 return Err(anyhow::anyhow!(
301 "select text is not a string or null"
302 ));
303 }
304 };
305 }
306 if let Some(text) = text {
307 let text = text.string();
308 messages.push(Message {
309 name: None,
310 message: text.replace("\\n", "\n"),
311 });
312 }
313 }
314 }
315 }
316 comu.as_mut().map(|c| c.export(&oscene));
317 }
318 if let Some(comu) = comu {
319 if !comu.messages.is_empty() {
320 let mut pb = std::path::PathBuf::from(&self.filename);
321 let filename = pb
322 .file_stem()
323 .map(|s| s.to_string_lossy())
324 .unwrap_or(std::borrow::Cow::from("comumode"));
325 pb.set_file_name(format!("{}_comumode.json", filename));
326 match std::fs::File::create(&pb) {
327 Ok(mut f) => {
328 let messages: Vec<String> = comu.messages.into_iter().collect();
329 if let Err(e) = serde_json::to_writer_pretty(&mut f, &messages) {
330 eprintln!("Failed to write COMU messages to {}: {:?}", pb.display(), e);
331 crate::COUNTER.inc_warning();
332 }
333 }
334 Err(e) => {
335 eprintln!(
336 "Failed to create COMU messages file {}: {:?}",
337 pb.display(),
338 e
339 );
340 crate::COUNTER.inc_warning();
341 }
342 }
343 }
344 }
345 Ok(messages)
346 }
347
348 fn import_messages<'a>(
349 &'a self,
350 messages: Vec<Message>,
351 file: Box<dyn WriteSeek + 'a>,
352 _filename: &str,
353 _encoding: Encoding,
354 replacement: Option<&'a ReplacementTable>,
355 ) -> Result<()> {
356 let mut mes = messages.iter();
357 let mut cur_mes = mes.next();
358 let mut psb = self.psb.clone();
359 let root = psb.root_mut();
360 let scenes = &mut root["scenes"];
361 if !scenes.is_list() {
362 return Err(anyhow::anyhow!("scenes is not an array"));
363 }
364 let comu = self
365 .comumode_json
366 .as_ref()
367 .map(|json| ImportComuMes::new(json, replacement));
368 for (i, scene) in scenes.members_mut().enumerate() {
369 if !scene.is_object() {
370 return Err(anyhow::anyhow!("scene at {} is not an object", i));
371 }
372 for (j, text) in scene["texts"].members_mut().enumerate() {
373 if text.is_list() {
374 if text.len() <= 1 {
375 continue; }
377 if cur_mes.is_none() {
378 cur_mes = mes.next();
379 }
380 if !text[0].is_string_or_null() {
381 return Err(anyhow::anyhow!("name is not a string or null"));
382 }
383 let has_name = text[0].is_string();
384 let mut has_display_name;
385 if text[1].is_list() {
386 if text[1].is_string() {
387 let m = match cur_mes.take() {
388 Some(m) => m,
389 None => {
390 return Err(anyhow::anyhow!(
391 "No enough messages. (text {j} at scene {i})"
392 ));
393 }
394 };
395 if has_name {
396 if let Some(name) = &m.name {
397 let mut name = name.clone();
398 if let Some(replacement) = replacement {
399 for (key, value) in replacement.map.iter() {
400 name = name.replace(key, value);
401 }
402 }
403 text[0].set_string(name);
404 } else {
405 return Err(anyhow::anyhow!(
406 "Name is missing for message. (text {j} at scene {i})"
407 ));
408 }
409 }
410 let mut message = m.message.clone();
411 if let Some(replacement) = replacement {
412 for (key, value) in replacement.map.iter() {
413 message = message.replace(key, value);
414 }
415 }
416 text[1].set_string(message.replace("\n", "\\n"));
417 } else if text[1].is_list() {
418 if text[1].len() > self.language_index
419 && text[1][self.language_index].is_list()
420 && text[1][self.language_index].len() >= 2
421 {
422 if !text[1][self.language_index][0].is_string_or_null() {
423 return Err(anyhow::anyhow!(
424 "display name is not a string or null"
425 ));
426 }
427 has_display_name = text[1][self.language_index][0].is_string();
428 if text[1][self.language_index][1].is_string() {
429 let m = match cur_mes.take() {
430 Some(m) => m,
431 None => {
432 return Err(anyhow::anyhow!(
433 "No enough messages. (text {j} at scene {i})"
434 ));
435 }
436 };
437 if has_name {
438 if let Some(name) = &m.name {
439 let mut name = name.clone();
440 if let Some(replacement) = replacement {
441 for (key, value) in replacement.map.iter() {
442 name = name.replace(key, value);
443 }
444 }
445 if has_display_name {
446 text[1][self.language_index][0].set_string(name);
447 } else {
448 text[0].set_string(name);
449 }
450 } else {
451 return Err(anyhow::anyhow!(
452 "Name is missing for message. (text {j} at scene {i})"
453 ));
454 }
455 }
456 let mut message = m.message.clone();
457 if let Some(replacement) = replacement {
458 for (key, value) in replacement.map.iter() {
459 message = message.replace(key, value);
460 }
461 }
462 text[1][self.language_index][1]
463 .set_string(message.replace("\n", "\\n"));
464 }
465 }
466 }
467 } else {
468 if text.len() <= 2 {
469 continue; }
471 if !text[1].is_string_or_null() {
472 return Err(anyhow::anyhow!("display name is not a string or null"));
473 }
474 has_display_name = text[1].is_string();
475 if text[2].is_string() {
476 let m = match cur_mes.take() {
477 Some(m) => m,
478 None => {
479 return Err(anyhow::anyhow!(
480 "No enough messages.(text {j} at scene {i})"
481 ));
482 }
483 };
484 if has_name {
485 if let Some(name) = &m.name {
486 let mut name = name.clone();
487 if let Some(replacement) = replacement {
488 for (key, value) in replacement.map.iter() {
489 name = name.replace(key, value);
490 }
491 }
492 if has_display_name {
493 text[1].set_string(name);
494 } else {
495 text[0].set_string(name);
496 }
497 } else {
498 return Err(anyhow::anyhow!(
499 "Name is missing for message.(text {j} at scene {i})"
500 ));
501 }
502 }
503 let mut message = m.message.clone();
504 if let Some(replacement) = replacement {
505 for (key, value) in replacement.map.iter() {
506 message = message.replace(key, value);
507 }
508 }
509 text[2].set_string(message.replace("\n", "\\n"));
510 } else if text[2].is_list() {
511 if text[2].len() > self.language_index
512 && text[2][self.language_index].is_list()
513 && text[2][self.language_index].len() >= 2
514 {
515 if !text[2][self.language_index][0].is_string_or_null() {
516 return Err(anyhow::anyhow!(
517 "display name is not a string or null"
518 ));
519 }
520 has_display_name = text[2][self.language_index][0].is_string();
521 if text[2][self.language_index][1].is_string() {
522 let m = match cur_mes.take() {
523 Some(m) => m,
524 None => {
525 return Err(anyhow::anyhow!(
526 "No enough messages.(text {j} at scene {i})"
527 ));
528 }
529 };
530 if has_name {
531 if let Some(name) = &m.name {
532 let mut name = name.clone();
533 if let Some(replacement) = replacement {
534 for (key, value) in replacement.map.iter() {
535 name = name.replace(key, value);
536 }
537 }
538 if has_display_name {
539 text[2][self.language_index][0].set_string(name);
540 } else {
541 text[0].set_string(name);
542 }
543 } else {
544 return Err(anyhow::anyhow!(
545 "Name is missing for message.(text {j} at scene {i})"
546 ));
547 }
548 }
549 let mut message = m.message.clone();
550 if let Some(replacement) = replacement {
551 for (key, value) in replacement.map.iter() {
552 message = message.replace(key, value);
553 }
554 }
555 text[2][self.language_index][1]
556 .set_string(message.replace("\n", "\\n"));
557 }
558 }
559 }
560 }
561 }
562 }
563 for select in scene["selects"].members_mut() {
564 if select.is_object() {
565 if cur_mes.is_none() {
566 cur_mes = mes.next();
567 }
568 if select["language"].is_list()
569 && select["language"].len() > self.language_index
570 && select["language"][self.language_index].is_object()
571 {
572 let lang_obj = &mut select["language"][self.language_index];
573 if lang_obj["text"].is_string() {
574 let m = match cur_mes.take() {
575 Some(m) => m,
576 None => {
577 return Err(anyhow::anyhow!("No enough messages."));
578 }
579 };
580 let mut text = m.message.clone();
581 if let Some(replacement) = replacement {
582 for (key, value) in replacement.map.iter() {
583 text = text.replace(key, value);
584 }
585 }
586 lang_obj["text"].set_string(text.replace("\n", "\\n"));
587 continue;
588 }
589 }
590 if select["text"].is_string() {
591 let m = match cur_mes.take() {
592 Some(m) => m,
593 None => {
594 return Err(anyhow::anyhow!("No enough messages."));
595 }
596 };
597 let mut text = m.message.clone();
598 if let Some(replacement) = replacement {
599 for (key, value) in replacement.map.iter() {
600 text = text.replace(key, value);
601 }
602 }
603 select["text"].set_string(text.replace("\n", "\\n"));
604 }
605 }
606 }
607 comu.as_ref().map(|c| c.import(scene));
608 }
609 if cur_mes.is_some() || mes.next().is_some() {
610 return Err(anyhow::anyhow!("Some messages were not processed."));
611 }
612 let psb = psb.to_psb();
613 let writer = PsbWriter::new(psb, file);
614 writer.finish().map_err(|e| {
615 anyhow::anyhow!("Failed to write PSB to file {}: {:?}", self.filename, e)
616 })?;
617 Ok(())
618 }
619
620 fn custom_export(&self, filename: &Path, encoding: Encoding) -> Result<()> {
621 let s = if self.custom_yaml {
622 serde_yaml_ng::to_string(&self.psb)
623 .map_err(|e| anyhow::anyhow!("Failed to serialize to YAML: {}", e))?
624 } else {
625 json::stringify_pretty(self.psb.to_json(), 2)
626 };
627 let mut f = crate::utils::files::write_file(filename)?;
628 let b = encode_string(encoding, &s, false)?;
629 f.write_all(&b)?;
630 Ok(())
631 }
632
633 fn custom_import<'a>(
634 &'a self,
635 custom_filename: &'a str,
636 file: Box<dyn WriteSeek + 'a>,
637 _encoding: Encoding,
638 output_encoding: Encoding,
639 ) -> Result<()> {
640 let data = crate::utils::files::read_file(custom_filename)?;
641 let s = decode_to_string(output_encoding, &data, true)?;
642 let psb = if self.custom_yaml {
643 let data: VirtualPsbFixedData = serde_yaml_ng::from_str(&s)
644 .map_err(|e| anyhow::anyhow!("Failed to deserialize YAML: {}", e))?;
645 let mut psb = self.psb.clone();
646 psb.set_data(data);
647 psb.to_psb()
648 } else {
649 let json = json::parse(&s)?;
650 let mut psb = self.psb.clone();
651 psb.from_json(&json)?;
652 psb.to_psb()
653 };
654 let writer = PsbWriter::new(psb, file);
655 writer.finish().map_err(|e| {
656 anyhow::anyhow!("Failed to write PSB to file {}: {:?}", self.filename, e)
657 })?;
658 Ok(())
659 }
660}
661
662#[derive(Debug)]
663struct ExportComuMes {
664 pub messages: HashSet<String>,
665}
666
667impl ExportComuMes {
668 pub fn new() -> Self {
669 Self {
670 messages: HashSet::new(),
671 }
672 }
673
674 pub fn export(&mut self, value: &PsbValueFixed) {
675 match value {
676 PsbValueFixed::Object(obj) => {
677 for (k, v) in obj.iter() {
678 if k == "comumode" {
679 if let PsbValueFixed::List(list) = v {
680 for item in list.iter() {
681 if let PsbValueFixed::Object(obj) = item {
682 if let Some(PsbValueFixed::String(s)) = obj.get_value("text") {
683 self.messages.insert(s.string().replace("\\n", "\n"));
684 }
685 }
686 }
687 }
688 } else {
689 self.export(v);
690 }
691 }
692 }
693 PsbValueFixed::List(list) => {
694 let list = list.values();
695 if list.len() > 1 {
696 if let PsbValueFixed::String(s) = &list[0] {
697 if s.string() == "comumode" {
698 for i in 1..list.len() {
699 if let PsbValueFixed::String(s) = &list[i - 1] {
700 if s.string() == "text" {
701 if let PsbValueFixed::String(text) = &list[i] {
702 self.messages
703 .insert(text.string().replace("\\n", "\n"));
704 }
705 }
706 }
707 }
708 return;
709 }
710 }
711 }
712 for item in list {
713 self.export(item);
714 }
715 }
716 _ => {}
717 }
718 }
719}
720
721#[derive(Debug)]
722struct ImportComuMes<'a> {
723 messages: &'a Arc<HashMap<String, String>>,
724 replacement: Option<&'a ReplacementTable>,
725}
726
727impl<'a> ImportComuMes<'a> {
728 pub fn new(
729 messages: &'a Arc<HashMap<String, String>>,
730 replacement: Option<&'a ReplacementTable>,
731 ) -> Self {
732 Self {
733 messages,
734 replacement,
735 }
736 }
737
738 pub fn import(&self, value: &mut PsbValueFixed) {
739 match value {
740 PsbValueFixed::Object(obj) => {
741 for (k, v) in obj.iter_mut() {
742 if k == "comumode" {
743 for obj in v.members_mut() {
744 if let Some(text) = obj["text"].as_str() {
745 if let Some(replace_text) = self.messages.get(text) {
746 let mut text = replace_text.clone();
747 if let Some(replacement) = self.replacement {
748 for (key, value) in replacement.map.iter() {
749 text = text.replace(key, value);
750 }
751 }
752 obj["text"].set_string(text.replace("\n", "\\n"));
753 } else {
754 eprintln!(
755 "Warning: COMU message '{}' not found in translation table.",
756 text
757 );
758 crate::COUNTER.inc_warning();
759 }
760 }
761 }
762 } else {
763 self.import(v);
764 }
765 }
766 }
767 PsbValueFixed::List(list) => {
768 if list.len() > 1 {
769 if list[0] == "comumode" {
770 for i in 1..list.len() {
771 if list[i - 1] == "text" {
772 if let Some(text) = list[i].as_str() {
773 if let Some(replace_text) = self.messages.get(text) {
774 let mut text = replace_text.clone();
775 if let Some(replacement) = self.replacement {
776 for (key, value) in replacement.map.iter() {
777 text = text.replace(key, value);
778 }
779 }
780 list[i].set_string(text.replace("\n", "\\n"));
781 } else {
782 eprintln!(
783 "Warning: COMU message '{}' not found in translation table.",
784 text
785 );
786 crate::COUNTER.inc_warning();
787 }
788 }
789 }
790 }
791 return;
792 }
793 }
794 for item in list.iter_mut() {
795 self.import(item);
796 }
797 }
798 _ => {}
799 }
800 }
801}