1use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10
11use siglus_assets::{
12 cgm::CgTableData,
13 dbs::DbsDatabase,
14 gameexe::{
15 GameexeConfig, GameexeDecodeOptions, GameexeDecodeReport, decode_gameexe_dat_bytes,
16 normalize_gameexe_key,
17 },
18 thumb_table::ThumbTable,
19};
20
21use super::unknown::UnknownOpRecorder;
22const INIDEF_BTN_SE_CNT: usize = 16;
23const INIMAX_BTN_SE_CNT: usize = 256;
24const INIDEF_BTN_ACTION_CNT: usize = 16;
25const INIMAX_BTN_ACTION_CNT: usize = 256;
26const TNM_BTN_STATE_MAX: usize = 5;
27const INIDEF_SE_CNT: usize = 16;
28const INIMIN_SE_CNT: usize = 8;
29const INIMAX_SE_CNT: usize = 256;
30const INIDEF_MWND_CNT: usize = 2;
31const INIMAX_MWND_CNT: usize = 256;
32const INIDEF_WAKU_CNT: usize = 4;
33const INIMAX_WAKU_CNT: usize = 256;
34const INIDEF_MWND_WAKU_BTN_CNT: usize = 8;
35const INIMAX_MWND_WAKU_BTN_CNT: usize = 256;
36const INIDEF_MWND_WAKU_FACE_CNT: usize = 1;
37const INIMAX_MWND_WAKU_FACE_CNT: usize = 16;
38const INIDEF_MWND_WAKU_OBJECT_CNT: usize = 1;
39const INIMAX_MWND_WAKU_OBJECT_CNT: usize = 16;
40const INIDEF_ICON_CNT: usize = 16;
41const INIMAX_ICON_CNT: usize = 256;
42const INIDEF_SEL_BTN_CNT: usize = 16;
43const INIMIN_SEL_BTN_CNT: usize = 0;
44const INIMAX_SEL_BTN_CNT: usize = 256;
45const INIDEF_SHAKE_CNT: usize = 16;
46const INIMAX_SHAKE_CNT: usize = 256;
47
48#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
49pub struct ShakeStep {
50 pub x: i32,
51 pub y: i32,
52 pub time_ms: i32,
53}
54
55#[derive(Debug, Clone, Copy)]
56pub struct ButtonSeTemplate {
57 pub hit_no: i64,
58 pub push_no: i64,
59 pub decide_no: i64,
60}
61
62impl Default for ButtonSeTemplate {
63 fn default() -> Self {
64 Self {
65 hit_no: 0,
66 push_no: -1,
67 decide_no: 1,
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy)]
73pub struct ButtonActionPattern {
74 pub rep_pat_no: i64,
75 pub rep_pos_x: i64,
76 pub rep_pos_y: i64,
77 pub rep_tr: i64,
78 pub rep_bright: i64,
79 pub rep_dark: i64,
80}
81
82impl Default for ButtonActionPattern {
83 fn default() -> Self {
84 Self {
85 rep_pat_no: 0,
86 rep_pos_x: 0,
87 rep_pos_y: 0,
88 rep_tr: 255,
89 rep_bright: 0,
90 rep_dark: 0,
91 }
92 }
93}
94
95#[derive(Debug, Clone, Copy)]
96pub struct ButtonActionTemplate {
97 pub state: [ButtonActionPattern; TNM_BTN_STATE_MAX],
98}
99
100impl Default for ButtonActionTemplate {
101 fn default() -> Self {
102 let mut state = [ButtonActionPattern::default(); TNM_BTN_STATE_MAX];
103 state[1].rep_bright = 32;
104 state[2].rep_bright = 32;
105 state[2].rep_pos_x = 1;
106 state[2].rep_pos_y = 1;
107 Self { state }
108 }
109}
110
111#[derive(Debug, Clone)]
112pub struct MwndTemplate {
113 pub novel_mode: i64,
114 pub extend_type: i64,
115 pub window_pos: (i64, i64),
116 pub window_size: (i64, i64),
117 pub message_pos: (i64, i64),
118 pub message_margin: (i64, i64, i64, i64),
119 pub moji_cnt: (i64, i64),
120 pub moji_size: i64,
121 pub moji_space: (i64, i64),
122 pub moji_color: i64,
123 pub shadow_color: i64,
124 pub fuchi_color: i64,
125 pub ruby_size: i64,
126 pub ruby_space: i64,
127 pub waku_no: i64,
128 pub waku_pos: (i64, i64),
129 pub name_disp_mode: i64,
130 pub name_newline: i64,
131 pub name_bracket: i64,
132 pub name_moji_size: i64,
133 pub name_moji_space: (i64, i64),
134 pub name_moji_cnt: i64,
135 pub name_window_pos: (i64, i64),
136 pub name_window_size: (i64, i64),
137 pub name_msg_pos: (i64, i64),
138 pub name_msg_pos_rep: (i64, i64),
139 pub name_msg_margin: (i64, i64, i64, i64),
140 pub name_extend_type: i64,
141 pub name_window_align: i64,
142 pub name_moji_color: i64,
143 pub name_shadow_color: i64,
144 pub name_fuchi_color: i64,
145 pub name_waku_no: i64,
146 pub face_hide_name: i64,
147 pub talk_margin: (i64, i64, i64, i64),
148 pub overflow_check_size: i64,
149 pub msg_back_insert_nl: i64,
150 pub open_anime_type: i64,
151 pub open_anime_time: i64,
152 pub close_anime_type: i64,
153 pub close_anime_time: i64,
154}
155
156impl Default for MwndTemplate {
157 fn default() -> Self {
158 Self {
159 novel_mode: 0,
160 extend_type: 0,
161 window_pos: (50, 400),
162 window_size: (700, 150),
163 message_pos: (20, 20),
164 message_margin: (20, 20, 20, 20),
165 moji_cnt: (26, 3),
166 moji_size: 25,
167 moji_space: (-1, 10),
168 moji_color: -1,
169 shadow_color: -1,
170 fuchi_color: -1,
171 ruby_size: 10,
172 ruby_space: 1,
173 waku_no: 0,
174 waku_pos: (0, 0),
175 name_disp_mode: 0,
176 name_newline: 0,
177 name_bracket: 0,
178 name_moji_size: 16,
179 name_moji_space: (-1, 8),
180 name_moji_cnt: 8,
181 name_window_pos: (0, -100),
182 name_window_size: (300, 100),
183 name_msg_pos: (8, 8),
184 name_msg_pos_rep: (0, 0),
185 name_msg_margin: (8, 8, 8, 8),
186 name_extend_type: 0,
187 name_window_align: 0,
188 name_moji_color: -1,
189 name_shadow_color: -1,
190 name_fuchi_color: -1,
191 name_waku_no: -1,
192 face_hide_name: 0,
193 talk_margin: (0, 0, 0, 0),
194 overflow_check_size: 0,
195 msg_back_insert_nl: 0,
196 open_anime_type: 0,
197 open_anime_time: 0,
198 close_anime_type: 0,
199 close_anime_time: 0,
200 }
201 }
202}
203
204#[derive(Debug, Clone, Copy)]
205pub struct MwndRenderTemplate {
206 pub vertical_writing: bool,
208 pub default_mwnd_no: i64,
209 pub default_sel_mwnd_no: i64,
210 pub order: i64,
211 pub filter_layer_rep: i64,
212 pub waku_layer_rep: i64,
213 pub face_layer_rep: i64,
214 pub shadow_layer_rep: i64,
215 pub fuchi_layer_rep: i64,
216 pub moji_layer_rep: i64,
217 pub shadow_color: i64,
218 pub fuchi_color: i64,
219 pub moji_color: i64,
220}
221
222impl Default for MwndRenderTemplate {
223 fn default() -> Self {
224 Self {
225 vertical_writing: false,
226 default_mwnd_no: 0,
227 default_sel_mwnd_no: 1,
228 order: 1,
229 filter_layer_rep: 0,
230 waku_layer_rep: 1,
231 face_layer_rep: 2,
232 shadow_layer_rep: 3,
233 fuchi_layer_rep: 4,
234 moji_layer_rep: 5,
235 shadow_color: 1,
236 fuchi_color: 1,
237 moji_color: 0,
238 }
239 }
240}
241
242#[derive(Debug, Clone, Default)]
243pub struct EmojiTemplate {
244 pub file_name: String,
245 pub font_size: i64,
246}
247
248#[derive(Debug, Clone, Copy)]
249pub struct MessageButtonTemplate {
250 pub color_no: [i64; TNM_BTN_STATE_MAX],
251}
252
253impl Default for MessageButtonTemplate {
254 fn default() -> Self {
255 let mut color_no = [0; TNM_BTN_STATE_MAX];
256 color_no[0] = 0;
257 color_no[1] = 2;
258 color_no[2] = 2;
259 Self { color_no }
260 }
261}
262
263#[derive(Debug, Clone)]
264pub struct IconTemplate {
265 pub file_name: String,
266 pub anime_pat_cnt: i64,
267 pub anime_speed: i64,
268}
269
270impl Default for IconTemplate {
271 fn default() -> Self {
272 Self {
273 file_name: String::new(),
274 anime_pat_cnt: 1,
275 anime_speed: 100,
276 }
277 }
278}
279
280#[derive(Debug, Clone)]
281pub struct NamaeEntry {
282 pub source: String,
283 pub display: String,
284 pub color_mod: i64,
285 pub moji_color_no: i64,
286 pub shadow_color_no: i64,
287 pub fuchi_color_no: i64,
288}
289
290#[derive(Debug, Clone, Copy, Default)]
291pub struct FontConfigDefaults {
292 pub font_type: i64,
293 pub futoku: i64,
294 pub shadow: i64,
295}
296
297#[derive(Debug, Clone, Default)]
298pub struct WakuButtonTemplate {
299 pub file_name: String,
300 pub cut_no: i64,
301 pub pos_base: i64,
302 pub pos: (i64, i64),
303 pub action_no: i64,
304 pub se_no: i64,
305 pub sys_type: i64,
306 pub sys_type_opt: i64,
307 pub btn_mode: i64,
308 pub scn_name: String,
309 pub cmd_name: String,
310 pub z_no: i64,
311 pub frame_action_scn_name: String,
312 pub frame_action_cmd_name: String,
313}
314
315#[derive(Debug, Clone)]
316pub struct WakuTemplate {
317 pub extend_type: i64,
318 pub waku_file: String,
319 pub filter_file: String,
320 pub filter_margin: (i64, i64, i64, i64),
321 pub filter_color: (u8, u8, u8, u8),
322 pub filter_config_color: bool,
323 pub filter_config_tr: bool,
324 pub icon_no: i64,
325 pub page_icon_no: i64,
326 pub icon_pos_type: i64,
327 pub icon_pos_base: i64,
328 pub icon_pos: (i64, i64, i64),
329 pub buttons: Vec<WakuButtonTemplate>,
330 pub face_pos: Vec<(i64, i64)>,
331 pub object_cnt: usize,
332}
333
334impl Default for WakuTemplate {
335 fn default() -> Self {
336 Self {
337 extend_type: 0,
338 waku_file: String::new(),
339 filter_file: String::new(),
340 filter_margin: (0, 0, 0, 0),
341 filter_color: (0, 0, 255, 128),
342 filter_config_color: true,
343 filter_config_tr: true,
344 icon_no: -1,
345 page_icon_no: -1,
346 icon_pos_type: 0,
347 icon_pos_base: 0,
348 icon_pos: (0, 0, 0),
349 buttons: vec![WakuButtonTemplate::default(); INIDEF_MWND_WAKU_BTN_CNT],
350 face_pos: vec![(0, 0); INIDEF_MWND_WAKU_FACE_CNT],
351 object_cnt: INIDEF_MWND_WAKU_OBJECT_CNT,
352 }
353 }
354}
355
356#[derive(Debug, Clone)]
357pub struct SelBtnTemplate {
358 pub base_file: String,
359 pub filter_file: String,
360 pub base_pos: (i64, i64),
361 pub rep_pos: (i64, i64),
362 pub x_align: i64,
363 pub y_align: i64,
364 pub max_y_cnt: i64,
365 pub line_width: i64,
366 pub moji_cnt: i64,
367 pub moji_pos: (i64, i64),
368 pub moji_size: i64,
369 pub moji_space: (i64, i64),
370 pub moji_x_align: i64,
371 pub moji_y_align: i64,
372 pub moji_color: i64,
373 pub moji_hit_color: i64,
374 pub btn_action_no: i64,
375 pub open_anime_type: i64,
376 pub open_anime_time: i64,
377 pub close_anime_type: i64,
378 pub close_anime_time: i64,
379 pub decide_anime_type: i64,
380 pub decide_anime_time: i64,
381}
382
383impl Default for SelBtnTemplate {
384 fn default() -> Self {
385 Self {
386 base_file: String::new(),
387 filter_file: String::new(),
388 base_pos: (0, 0),
389 rep_pos: (0, 0),
390 x_align: 0,
391 y_align: 0,
392 max_y_cnt: 0,
393 line_width: 100,
394 moji_cnt: 0,
395 moji_pos: (0, 0),
396 moji_size: 25,
397 moji_space: (0, 0),
398 moji_x_align: 0,
399 moji_y_align: 0,
400 moji_color: 0,
401 moji_hit_color: 5,
402 btn_action_no: 0,
403 open_anime_type: 1,
404 open_anime_time: 500,
405 close_anime_type: 1,
406 close_anime_time: 500,
407 decide_anime_type: 1,
408 decide_anime_time: 500,
409 }
410 }
411}
412
413#[derive(Debug)]
414pub struct AssetTables {
415 pub gameexe: Option<GameexeConfig>,
416 pub gameexe_report: Option<GameexeDecodeReport>,
417
418 pub button_se_templates: Vec<ButtonSeTemplate>,
419 pub button_action_templates: Vec<ButtonActionTemplate>,
420 pub se_file_names: Vec<Option<String>>,
421 pub mwnd_render: MwndRenderTemplate,
422 pub mwnd_templates: Vec<MwndTemplate>,
423 pub emoji_templates: Vec<EmojiTemplate>,
424 pub message_button_templates: Vec<MessageButtonTemplate>,
425 pub waku_templates: Vec<WakuTemplate>,
426 pub icon_templates: Vec<IconTemplate>,
427 pub sel_btn_templates: Vec<SelBtnTemplate>,
428 pub namae_entries: Vec<NamaeEntry>,
429 pub color_table: Vec<(u8, u8, u8)>,
430 pub font_defaults: FontConfigDefaults,
431 pub shake_templates: Vec<Vec<ShakeStep>>,
434
435 pub cgtable: Option<CgTableData>,
436 pub cgtable_flag_cnt: Option<usize>,
437 pub cg_flags: Vec<i32>,
439
440 pub databases: Vec<Option<DbsDatabase>>,
441
442 pub thumb_table: Option<ThumbTable>,
443}
444
445impl Default for AssetTables {
446 fn default() -> Self {
447 Self {
448 gameexe: None,
449 gameexe_report: None,
450 button_se_templates: vec![ButtonSeTemplate::default(); INIDEF_BTN_SE_CNT],
451 button_action_templates: vec![ButtonActionTemplate::default(); INIDEF_BTN_ACTION_CNT],
452 se_file_names: vec![None; INIDEF_SE_CNT],
453 mwnd_render: MwndRenderTemplate::default(),
454 mwnd_templates: vec![MwndTemplate::default(); INIDEF_MWND_CNT],
455 emoji_templates: vec![EmojiTemplate::default(); 16],
456 message_button_templates: vec![MessageButtonTemplate::default(); 16],
457 waku_templates: vec![WakuTemplate::default(); INIDEF_WAKU_CNT],
458 icon_templates: vec![IconTemplate::default(); INIDEF_ICON_CNT],
459 sel_btn_templates: vec![SelBtnTemplate::default(); INIDEF_SEL_BTN_CNT],
460 namae_entries: Vec::new(),
461 color_table: default_color_table(),
462 font_defaults: FontConfigDefaults::default(),
463 shake_templates: vec![Vec::new(); INIDEF_SHAKE_CNT],
464 cgtable: None,
465 cgtable_flag_cnt: None,
466 cg_flags: Vec::new(),
467 databases: Vec::new(),
468 thumb_table: None,
469 }
470 }
471}
472
473impl AssetTables {
474 pub fn load(project_dir: &Path, unknown: &mut UnknownOpRecorder) -> Self {
475 let mut out = Self::default();
476
477 let gameexe_path = match crate::resource::find_initial_gameexe_path(project_dir) {
478 Ok(path) => path,
479 Err(_) => {
480 unknown.record_note("gameexe.missing");
481 return out;
482 }
483 };
484
485 let raw = match crate::resource::read_file_bytes(&gameexe_path) {
486 Ok(b) => b,
487 Err(e) => {
488 unknown.record_note(&format!("gameexe.read.failed:{e}"));
489 return out;
490 }
491 };
492
493 let (text, report): (String, Option<GameexeDecodeReport>) = if gameexe_path
494 .extension()
495 .and_then(|s| s.to_str())
496 .is_some_and(|ext| ext.eq_ignore_ascii_case("ini"))
497 {
498 match String::from_utf8(raw) {
499 Ok(text) => (text, None),
500 Err(e) => {
501 unknown.record_note(&format!("gameexe.ini.decode.failed:{e}"));
502 return out;
503 }
504 }
505 } else {
506 let opt = match load_gameexe_decode_options(project_dir) {
507 Ok(v) => v,
508 Err(e) => {
509 unknown.record_note(&format!("gameexe.key_toml.invalid:{e}"));
511 GameexeDecodeOptions::default()
512 }
513 };
514
515 match decode_gameexe_dat_bytes(&raw, &opt) {
516 Ok((text, report)) => (text, Some(report)),
517 Err(e) => {
518 unknown.record_note(&format!("gameexe.decode.failed:{e}"));
519 return out;
520 }
521 }
522 };
523
524 let cfg = GameexeConfig::from_text(&text);
525 out.gameexe_report = report;
526 out.button_se_templates = load_button_se_templates(&cfg);
527 out.button_action_templates = load_button_action_templates(&cfg);
528 out.se_file_names = load_se_file_names(&cfg);
529 out.mwnd_render = load_mwnd_render_template(&cfg);
530 out.mwnd_templates = load_mwnd_templates(&cfg);
531 out.emoji_templates = load_emoji_templates(&cfg);
532 out.message_button_templates = load_message_button_templates(&cfg);
533 out.waku_templates = load_waku_templates(&cfg, Some(&text));
534 out.icon_templates = load_icon_templates(&cfg);
535 out.sel_btn_templates = load_sel_btn_templates(&cfg);
536 out.namae_entries = load_namae_entries(Some(&text));
537 out.color_table = load_color_table(&cfg);
538 out.font_defaults = load_font_config_defaults(&cfg);
539 out.shake_templates = load_shake_templates(&cfg);
540 out.gameexe = Some(cfg);
541
542 let dat_dir = project_dir.join("dat");
544 if !path_is_dir(&dat_dir) {
545 unknown.record_note("dat.dir.missing");
546 }
547
548 if let Some(cfg) = out.gameexe.as_ref() {
549 if let Some(v) = cfg.get_unquoted("CGTABLE_FILE") {
551 if let Some(path) = resolve_table_path(project_dir, &dat_dir, v, Some("cgm")) {
552 match crate::resource::read_file_bytes(&path)
553 .and_then(|bytes| CgTableData::from_bytes(&bytes))
554 {
555 Ok(t) => out.cgtable = Some(t),
556 Err(e) => unknown.record_note(&format!("cgtable.load.failed:{path:?}:{e}")),
557 }
558 } else {
559 unknown.record_note(&format!("cgtable.path.missing:{v}"));
560 }
561 }
562
563 if let Some(n) = cfg.get_usize("CGTABLE_FLAG_CNT") {
565 out.cgtable_flag_cnt = Some(n);
566 out.cg_flags = vec![0i32; n.max(32)];
567 }
568
569 if let Some(v) = cfg.get_unquoted("THUMBTABLE_FILE") {
571 if let Some(path) = resolve_table_path(project_dir, &dat_dir, v, Some("dat")) {
572 match crate::resource::read_file_bytes(&path)
573 .and_then(|bytes| ThumbTable::from_bytes(&bytes))
574 {
575 Ok(t) => out.thumb_table = Some(t),
576 Err(e) => {
577 unknown.record_note(&format!("thumb_table.load.failed:{path:?}:{e}"))
578 }
579 }
580 } else {
581 unknown.record_note(&format!("thumb_table.path.missing:{v}"));
582 }
583 }
584
585 let db_cnt = cfg.indexed_count("DATABASE");
587 out.databases = vec![None; db_cnt];
591 let database_append = crate::resource::initial_select_ini_append(project_dir);
592 for i in 0..db_cnt {
593 let key = format!("DATABASE.{i}");
594 let Some(name) = cfg
595 .get_indexed_unquoted("DATABASE", i)
596 .or_else(|| cfg.get_indexed_item_unquoted("DATABASE", i, 0))
597 .or_else(|| cfg.get_indexed_field_unquoted("DATABASE", i, "FILE"))
598 .or_else(|| cfg.get_indexed_field_unquoted("DATABASE", i, "NAME"))
599 else {
600 unknown.record_note(&format!("database.name.missing:{key}"));
601 continue;
602 };
603 let dbs_file_name = format!("{name}.dbs");
609 let path = match crate::resource::resolve_dat_file_path(
610 project_dir,
611 &database_append.dir,
612 &dbs_file_name,
613 ) {
614 Ok(Some(path)) => path,
615 Ok(None) => {
616 unknown
617 .record_note(&format!("database.path.missing:{key}:{dbs_file_name}"));
618 continue;
619 }
620 Err(e) => {
621 unknown.record_note(&format!(
622 "database.path.resolve.failed:{key}:{dbs_file_name}:{e}"
623 ));
624 continue;
625 }
626 };
627 match crate::resource::read_file_bytes(&path)
628 .and_then(|bytes| DbsDatabase::from_bytes(&bytes))
629 {
630 Ok(db) => out.databases[i] = Some(db),
631 Err(e) => unknown.record_note(&format!("dbs.load.failed:{path:?}:{e}")),
632 }
633 }
634 }
635
636 out
637 }
638}
639
640fn parse_i64_tuple(raw: Option<&str>) -> Vec<i64> {
641 raw.map(parse_button_action_numbers).unwrap_or_default()
642}
643
644fn load_mwnd_render_template(cfg: &GameexeConfig) -> MwndRenderTemplate {
645 let mut t = MwndRenderTemplate {
646 vertical_writing: cfg
647 .get_unquoted("TATEYOKO_MODE")
648 .and_then(parse_i64_like_local)
649 .unwrap_or(0)
650 == 1,
651 ..Default::default()
652 };
653
654 if let Some(v) = cfg
655 .get_unquoted("MWND.DEFAULT_MWND_NO")
656 .and_then(parse_i64_like_local)
657 {
658 t.default_mwnd_no = v;
659 }
660 if let Some(v) = cfg
661 .get_unquoted("MWND.DEFAULT_SEL_MWND_NO")
662 .and_then(parse_i64_like_local)
663 {
664 t.default_sel_mwnd_no = v;
665 }
666 if let Some(v) = cfg
667 .get_unquoted("MWND.ORDER")
668 .and_then(parse_i64_like_local)
669 {
670 t.order = v;
671 }
672 if let Some(v) = cfg
673 .get_unquoted("MWND.FILTER_LAYER_REP")
674 .and_then(parse_i64_like_local)
675 {
676 t.filter_layer_rep = v;
677 }
678 if let Some(v) = cfg
679 .get_unquoted("MWND.WAKU_LAYER_REP")
680 .and_then(parse_i64_like_local)
681 {
682 t.waku_layer_rep = v;
683 }
684 if let Some(v) = cfg
685 .get_unquoted("MWND.FACE_LAYER_REP")
686 .and_then(parse_i64_like_local)
687 {
688 t.face_layer_rep = v;
689 }
690 if let Some(v) = cfg
691 .get_unquoted("MWND.SHADOW_LAYER_REP")
692 .and_then(parse_i64_like_local)
693 {
694 t.shadow_layer_rep = v;
695 }
696 if let Some(v) = cfg
697 .get_unquoted("MWND.FUCHI_LAYER_REP")
698 .and_then(parse_i64_like_local)
699 {
700 t.fuchi_layer_rep = v;
701 }
702 if let Some(v) = cfg
703 .get_unquoted("MWND.MOJI_LAYER_REP")
704 .and_then(parse_i64_like_local)
705 {
706 t.moji_layer_rep = v;
707 }
708 if let Some(v) = cfg
709 .get_unquoted("MWND.SHADOW_COLOR")
710 .and_then(parse_i64_like_local)
711 {
712 t.shadow_color = v;
713 }
714 if let Some(v) = cfg
715 .get_unquoted("MWND.FUCHI_COLOR")
716 .and_then(parse_i64_like_local)
717 {
718 t.fuchi_color = v;
719 }
720 if let Some(v) = cfg
721 .get_unquoted("MWND.MOJI_COLOR")
722 .and_then(parse_i64_like_local)
723 {
724 t.moji_color = v;
725 }
726 t
727}
728
729fn load_mwnd_templates(cfg: &GameexeConfig) -> Vec<MwndTemplate> {
730 let cnt = cfg
731 .get_usize("MWND.CNT")
732 .unwrap_or(INIDEF_MWND_CNT)
733 .min(INIMAX_MWND_CNT);
734 let mut out = vec![MwndTemplate::default(); cnt];
735
736 for i in 0..cnt {
737 let mut t = MwndTemplate::default();
738 let get_i64 = |field: &str| {
739 cfg.get_indexed_field("MWND", i, field)
740 .and_then(parse_i64_like_local)
741 };
742 let get_tuple = |field: &str| parse_i64_tuple(cfg.get_indexed_field("MWND", i, field));
743
744 if let Some(v) = get_i64("NOVEL_MODE") {
745 t.novel_mode = v;
746 }
747 if let Some(v) = get_i64("EXTEND_TYPE") {
748 t.extend_type = v;
749 }
750 let window_pos = get_tuple("WINDOW_POS");
751 if window_pos.len() >= 2 {
752 t.window_pos = (window_pos[0], window_pos[1]);
753 }
754 let window_size = get_tuple("WINDOW_SIZE");
755 if window_size.len() >= 2 {
756 t.window_size = (window_size[0], window_size[1]);
757 }
758 let message_pos = get_tuple("MESSAGE_POS");
759 if message_pos.len() >= 2 {
760 t.message_pos = (message_pos[0], message_pos[1]);
761 }
762 let message_margin = get_tuple("MESSAGE_MARGIN");
763 if message_margin.len() >= 4 {
764 t.message_margin = (
765 message_margin[0],
766 message_margin[1],
767 message_margin[2],
768 message_margin[3],
769 );
770 }
771 let moji_cnt = get_tuple("MOJI_CNT");
772 if moji_cnt.len() >= 2 {
773 t.moji_cnt = (moji_cnt[0], moji_cnt[1]);
774 }
775 if let Some(v) = get_i64("MOJI_SIZE") {
776 t.moji_size = v;
777 }
778 let moji_space = get_tuple("MOJI_SPACE");
779 if moji_space.len() >= 2 {
780 t.moji_space = (moji_space[0], moji_space[1]);
781 }
782 if let Some(v) = get_i64("MOJI_COLOR") {
783 t.moji_color = v;
784 }
785 if let Some(v) = get_i64("SHADOW_COLOR") {
786 t.shadow_color = v;
787 }
788 if let Some(v) = get_i64("FUCHI_COLOR") {
789 t.fuchi_color = v;
790 }
791 if let Some(v) = get_i64("RUBY_SIZE") {
792 t.ruby_size = v;
793 }
794 if let Some(v) = get_i64("RUBY_SPACE") {
795 t.ruby_space = v;
796 }
797 if let Some(v) = get_i64("WAKU_NO") {
798 t.waku_no = v;
799 }
800 let waku_pos = get_tuple("WAKU_POS");
801 if waku_pos.len() >= 2 {
802 t.waku_pos = (waku_pos[0], waku_pos[1]);
803 }
804 if let Some(v) = get_i64("NAME_DISP_MODE") {
805 t.name_disp_mode = v;
806 }
807 if let Some(v) = get_i64("NAME_NEWLINE") {
808 t.name_newline = v;
809 }
810 if let Some(v) = get_i64("NAME_BRACKET") {
811 t.name_bracket = v;
812 }
813 if let Some(v) = get_i64("NAME_MOJI_SIZE") {
814 t.name_moji_size = v;
815 }
816 let name_moji_space = get_tuple("NAME_MOJI_SPACE");
817 if name_moji_space.len() >= 2 {
818 t.name_moji_space = (name_moji_space[0], name_moji_space[1]);
819 }
820 if let Some(v) = get_i64("NAME_MOJI_CNT") {
821 t.name_moji_cnt = v;
822 }
823 let name_window_pos = get_tuple("NAME_WINDOW_POS");
824 if name_window_pos.len() >= 2 {
825 t.name_window_pos = (name_window_pos[0], name_window_pos[1]);
826 }
827 let name_window_size = get_tuple("NAME_WINDOW_SIZE");
828 if name_window_size.len() >= 2 {
829 t.name_window_size = (name_window_size[0], name_window_size[1]);
830 }
831 let name_msg_pos = get_tuple("NAME_MESSAGE_POS");
832 if name_msg_pos.len() >= 2 {
833 t.name_msg_pos = (name_msg_pos[0], name_msg_pos[1]);
834 }
835 let name_msg_pos_rep = get_tuple("NAME_MESSAGE_POS_REP");
836 if name_msg_pos_rep.len() >= 2 {
837 t.name_msg_pos_rep = (name_msg_pos_rep[0], name_msg_pos_rep[1]);
838 }
839 let name_msg_margin = get_tuple("NAME_MESSAGE_MARGIN");
840 if name_msg_margin.len() >= 4 {
841 t.name_msg_margin = (
842 name_msg_margin[0],
843 name_msg_margin[1],
844 name_msg_margin[2],
845 name_msg_margin[3],
846 );
847 }
848 if let Some(v) = get_i64("NAME_MOJI_COLOR") {
849 t.name_moji_color = v;
850 }
851 if let Some(v) = get_i64("NAME_SHADOW_COLOR") {
852 t.name_shadow_color = v;
853 }
854 if let Some(v) = get_i64("NAME_FUCHI_COLOR") {
855 t.name_fuchi_color = v;
856 }
857 if let Some(v) = get_i64("NAME_WAKU_NO") {
858 t.name_waku_no = v;
859 }
860 if let Some(v) = get_i64("NAME_EXTEND_TYPE") {
861 t.name_extend_type = v;
862 }
863 if let Some(v) = get_i64("NAME_WINDOW_ALIGN") {
864 t.name_window_align = v;
865 }
866 if let Some(v) = get_i64("FACE_HIDE_NAME") {
867 t.face_hide_name = v;
868 }
869 let talk_margin = get_tuple("TALK_MARGIN");
870 if talk_margin.len() >= 4 {
871 t.talk_margin = (
872 talk_margin[0],
873 talk_margin[1],
874 talk_margin[2],
875 talk_margin[3],
876 );
877 }
878 if let Some(v) = get_i64("OVERFLOW_CHECK_SIZE") {
879 t.overflow_check_size = v;
880 }
881 if let Some(v) = get_i64("MSG_BACK_INSERT_NL") {
882 t.msg_back_insert_nl = v;
883 }
884 if let Some(v) = get_i64("OPEN_ANIME_TYPE") {
885 t.open_anime_type = v;
886 }
887 if let Some(v) = get_i64("OPEN_ANIME_TIME") {
888 t.open_anime_time = v;
889 }
890 if let Some(v) = get_i64("CLOSE_ANIME_TYPE") {
891 t.close_anime_type = v;
892 }
893 if let Some(v) = get_i64("CLOSE_ANIME_TIME") {
894 t.close_anime_time = v;
895 }
896 out[i] = t;
897 }
898
899 out
900}
901
902fn nested_indexed_field<'a>(
903 cfg: &'a GameexeConfig,
904 prefix: &str,
905 index: usize,
906 nested: &str,
907 nested_index: usize,
908 field: &str,
909) -> Option<&'a str> {
910 for direct in [
918 format!("{prefix}.{index}.{nested}.{nested_index}.{field}"),
919 format!("{prefix}.{index:03}.{nested}.{nested_index}.{field}"),
920 format!("{prefix}.{index}.{nested}.{nested_index:03}.{field}"),
921 format!("{prefix}.{index:03}.{nested}.{nested_index:03}.{field}"),
922 ] {
923 if let Some(v) = cfg.get_value(&direct) {
924 return Some(v);
925 }
926 }
927
928 for nested_prefix in [
929 format!("{prefix}.{index}.{nested}"),
930 format!("{prefix}.{index:03}.{nested}"),
931 ] {
932 if let Some(v) = cfg.get_indexed_field(&nested_prefix, nested_index, field) {
933 return Some(v);
934 }
935 }
936
937 for flat_field in [
938 format!("{nested}.{nested_index}.{field}"),
939 format!("{nested}.{nested_index:03}.{field}"),
940 ] {
941 if let Some(v) = cfg.get_indexed_field(prefix, index, &flat_field) {
942 return Some(v);
943 }
944 }
945 None
946}
947
948fn nested_indexed_field_unquoted<'a>(
949 cfg: &'a GameexeConfig,
950 prefix: &str,
951 index: usize,
952 nested: &str,
953 nested_index: usize,
954 field: &str,
955) -> Option<&'a str> {
956 for direct in [
959 format!("{prefix}.{index}.{nested}.{nested_index}.{field}"),
960 format!("{prefix}.{index:03}.{nested}.{nested_index}.{field}"),
961 format!("{prefix}.{index}.{nested}.{nested_index:03}.{field}"),
962 format!("{prefix}.{index:03}.{nested}.{nested_index:03}.{field}"),
963 ] {
964 if let Some(v) = cfg.get_unquoted(&direct) {
965 return Some(v);
966 }
967 }
968
969 for nested_prefix in [
970 format!("{prefix}.{index}.{nested}"),
971 format!("{prefix}.{index:03}.{nested}"),
972 ] {
973 if let Some(v) = cfg.get_indexed_field_unquoted(&nested_prefix, nested_index, field) {
974 return Some(v);
975 }
976 }
977
978 for flat_field in [
979 format!("{nested}.{nested_index}.{field}"),
980 format!("{nested}.{nested_index:03}.{field}"),
981 ] {
982 if let Some(v) = cfg.get_indexed_field_unquoted(prefix, index, &flat_field) {
983 return Some(v);
984 }
985 }
986 None
987}
988
989type RawGameexeFields<'a> = HashMap<String, &'a str>;
990
991fn canonical_raw_gameexe_key(raw: &str) -> String {
992 normalize_gameexe_key(raw)
993 .split('.')
994 .filter(|part| !part.is_empty())
995 .map(|part| {
996 part.parse::<usize>()
997 .map(|value| value.to_string())
998 .unwrap_or_else(|_| part.to_string())
999 })
1000 .collect::<Vec<_>>()
1001 .join(".")
1002}
1003
1004fn index_raw_gameexe_fields(raw_text: Option<&str>) -> RawGameexeFields<'_> {
1005 let mut fields = HashMap::new();
1006 for line in raw_text.unwrap_or_default().lines() {
1007 let mut s = line.trim();
1008 if s.is_empty() {
1009 continue;
1010 }
1011 if let Some(rest) = s.strip_prefix('\u{feff}') {
1012 s = rest.trim_start();
1013 }
1014 if let Some(rest) = s.strip_prefix('#') {
1015 s = rest.trim_start();
1016 }
1017 let Some((lhs, rhs)) = s.split_once('=') else {
1018 continue;
1019 };
1020 let value = rhs.trim().trim_end_matches(';').trim();
1021 fields.insert(canonical_raw_gameexe_key(lhs), value);
1025 }
1026 fields
1027}
1028
1029fn raw_gameexe_field(fields: &RawGameexeFields<'_>, key: &str) -> Option<String> {
1030 fields
1031 .get(&canonical_raw_gameexe_key(key))
1032 .map(|value| (*value).to_string())
1033}
1034
1035fn raw_nested_indexed_field(
1036 fields: &RawGameexeFields<'_>,
1037 prefix: &str,
1038 index: usize,
1039 nested: &str,
1040 nested_index: usize,
1041 field: &str,
1042) -> Option<String> {
1043 for key in [
1044 format!("{prefix}.{index}.{nested}.{nested_index}.{field}"),
1045 format!("{prefix}.{index:03}.{nested}.{nested_index}.{field}"),
1046 format!("{prefix}.{index}.{nested}.{nested_index:03}.{field}"),
1047 format!("{prefix}.{index:03}.{nested}.{nested_index:03}.{field}"),
1048 ] {
1049 if let Some(v) = raw_gameexe_field(fields, &key) {
1050 return Some(v);
1051 }
1052 }
1053 None
1054}
1055
1056fn raw_indexed_field(
1057 fields: &RawGameexeFields<'_>,
1058 prefix: &str,
1059 index: usize,
1060 field: &str,
1061) -> Option<String> {
1062 for key in [
1063 format!("{prefix}.{index}.{field}"),
1064 format!("{prefix}.{index:03}.{field}"),
1065 ] {
1066 if let Some(v) = raw_gameexe_field(fields, &key) {
1067 return Some(v);
1068 }
1069 }
1070 None
1071}
1072
1073fn trim_gameexe_scalar(raw: &str) -> &str {
1074 raw.trim().trim_matches('"')
1075}
1076
1077fn parse_waku_button_type(raw: &str, button: &mut WakuButtonTemplate) {
1078 let parts: Vec<&str> = raw.split(',').map(|p| p.trim().trim_matches('"')).collect();
1079 if parts.is_empty() {
1080 return;
1081 }
1082 let ty = parts[0].to_ascii_lowercase();
1083 let n0 = parts
1084 .get(1)
1085 .and_then(|v| parse_i64_like_local(v))
1086 .unwrap_or(0);
1087 let n1 = parts
1088 .get(2)
1089 .and_then(|v| parse_i64_like_local(v))
1090 .unwrap_or(0);
1091 match ty.as_str() {
1092 "none" => button.sys_type = 0,
1093 "save" => button.sys_type = 1,
1094 "load" => button.sys_type = 2,
1095 "read_skip" => {
1096 button.sys_type = 3;
1097 button.btn_mode = n0;
1098 }
1099 "auto_mode" => {
1100 button.sys_type = 4;
1101 button.btn_mode = n0;
1102 }
1103 "return_sel" => button.sys_type = 5,
1104 "close_mwnd" => button.sys_type = 6,
1105 "msg_log" => button.sys_type = 7,
1106 "koe_play" => button.sys_type = 8,
1107 "qsave" => {
1108 button.sys_type = 9;
1109 button.sys_type_opt = n0;
1110 }
1111 "qload" => {
1112 button.sys_type = 10;
1113 button.sys_type_opt = n0;
1114 }
1115 "config" => button.sys_type = 11,
1116 "local_switch" => {
1117 button.sys_type = 12;
1118 button.sys_type_opt = n0;
1119 button.btn_mode = n1;
1120 }
1121 "local_mode" => {
1122 button.sys_type = 13;
1123 button.sys_type_opt = n0;
1124 button.btn_mode = n1;
1125 }
1126 "global_switch" => {
1127 button.sys_type = 14;
1128 button.sys_type_opt = n0;
1129 button.btn_mode = n1;
1130 }
1131 "global_mode" => {
1132 button.sys_type = 15;
1133 button.sys_type_opt = n0;
1134 button.btn_mode = n1;
1135 }
1136 _ => {}
1137 }
1138}
1139
1140fn load_waku_templates(cfg: &GameexeConfig, raw_text: Option<&str>) -> Vec<WakuTemplate> {
1141 let raw_fields = index_raw_gameexe_fields(raw_text);
1144 let cnt = cfg
1145 .get_usize("WAKU.CNT")
1146 .unwrap_or(INIDEF_WAKU_CNT)
1147 .min(INIMAX_WAKU_CNT);
1148 let btn_cnt = cfg
1149 .get_usize("WAKU.BTN.CNT")
1150 .unwrap_or(INIDEF_MWND_WAKU_BTN_CNT)
1151 .min(INIMAX_MWND_WAKU_BTN_CNT);
1152 let face_cnt = cfg
1153 .get_usize("WAKU.FACE.CNT")
1154 .unwrap_or(INIDEF_MWND_WAKU_FACE_CNT)
1155 .min(INIMAX_MWND_WAKU_FACE_CNT);
1156 let object_cnt = cfg
1157 .get_usize("WAKU.OBJECT.CNT")
1158 .unwrap_or(INIDEF_MWND_WAKU_OBJECT_CNT)
1159 .min(INIMAX_MWND_WAKU_OBJECT_CNT);
1160 let mut out = vec![WakuTemplate::default(); cnt];
1161
1162 for i in 0..cnt {
1163 let mut t = WakuTemplate {
1164 buttons: vec![WakuButtonTemplate::default(); btn_cnt],
1165 face_pos: vec![(0, 0); face_cnt],
1166 object_cnt,
1167 ..Default::default()
1168 };
1169
1170 let raw_top = |field: &str| raw_indexed_field(&raw_fields, "WAKU", i, field);
1171
1172 let extend_type_raw = raw_top("EXTEND_TYPE");
1173 if let Some(v) = extend_type_raw
1174 .as_deref()
1175 .or_else(|| cfg.get_indexed_field("WAKU", i, "EXTEND_TYPE"))
1176 .and_then(parse_i64_like_local)
1177 {
1178 t.extend_type = v;
1179 }
1180 let waku_file_raw = raw_top("WAKU_FILE");
1181 if let Some(v) = waku_file_raw
1182 .as_deref()
1183 .or_else(|| cfg.get_indexed_field_unquoted("WAKU", i, "WAKU_FILE"))
1184 {
1185 t.waku_file = trim_gameexe_scalar(v).to_string();
1186 }
1187 let filter_file_raw = raw_top("FILTER_FILE");
1188 if let Some(v) = filter_file_raw
1189 .as_deref()
1190 .or_else(|| cfg.get_indexed_field_unquoted("WAKU", i, "FILTER_FILE"))
1191 {
1192 t.filter_file = trim_gameexe_scalar(v).to_string();
1193 }
1194 let filter_margin_raw = raw_top("FILTER_MARGIN");
1195 let filter_margin = parse_i64_tuple(
1196 filter_margin_raw
1197 .as_deref()
1198 .or_else(|| cfg.get_indexed_field("WAKU", i, "FILTER_MARGIN")),
1199 );
1200 if filter_margin.len() >= 4 {
1201 t.filter_margin = (
1202 filter_margin[0],
1203 filter_margin[1],
1204 filter_margin[2],
1205 filter_margin[3],
1206 );
1207 }
1208 let filter_color_raw = raw_top("FILTER_COLOR");
1209 let filter_color = parse_i64_tuple(
1210 filter_color_raw
1211 .as_deref()
1212 .or_else(|| cfg.get_indexed_field("WAKU", i, "FILTER_COLOR")),
1213 );
1214 if filter_color.len() >= 4 {
1215 t.filter_color = (
1216 filter_color[0].clamp(0, 255) as u8,
1217 filter_color[1].clamp(0, 255) as u8,
1218 filter_color[2].clamp(0, 255) as u8,
1219 filter_color[3].clamp(0, 255) as u8,
1220 );
1221 }
1222 let filter_config_color_raw = raw_top("FILTER_CONFIG_COLOR");
1223 if let Some(v) = filter_config_color_raw
1224 .as_deref()
1225 .or_else(|| cfg.get_indexed_field("WAKU", i, "FILTER_CONFIG_COLOR"))
1226 .and_then(parse_i64_like_local)
1227 {
1228 t.filter_config_color = v != 0;
1229 }
1230 let filter_config_tr_raw = raw_top("FILTER_CONFIG_TR");
1231 if let Some(v) = filter_config_tr_raw
1232 .as_deref()
1233 .or_else(|| cfg.get_indexed_field("WAKU", i, "FILTER_CONFIG_TR"))
1234 .and_then(parse_i64_like_local)
1235 {
1236 t.filter_config_tr = v != 0;
1237 }
1238 let icon_no_raw = raw_top("ICON_NO");
1239 if let Some(v) = icon_no_raw
1240 .as_deref()
1241 .or_else(|| cfg.get_indexed_field("WAKU", i, "ICON_NO"))
1242 .and_then(parse_i64_like_local)
1243 {
1244 t.icon_no = v;
1245 }
1246 let page_icon_no_raw = raw_top("PAGE_ICON_NO");
1247 if let Some(v) = page_icon_no_raw
1248 .as_deref()
1249 .or_else(|| cfg.get_indexed_field("WAKU", i, "PAGE_ICON_NO"))
1250 .and_then(parse_i64_like_local)
1251 {
1252 t.page_icon_no = v;
1253 }
1254 let icon_pos_type_raw = raw_top("ICON_POS_TYPE");
1255 if let Some(v) = icon_pos_type_raw
1256 .as_deref()
1257 .or_else(|| cfg.get_indexed_field("WAKU", i, "ICON_POS_TYPE"))
1258 .and_then(parse_i64_like_local)
1259 {
1260 t.icon_pos_type = v;
1261 }
1262 let icon_pos_base_raw = raw_top("ICON_POS_BASE");
1263 if let Some(v) = icon_pos_base_raw
1264 .as_deref()
1265 .or_else(|| cfg.get_indexed_field("WAKU", i, "ICON_POS_BASE"))
1266 .and_then(parse_i64_like_local)
1267 {
1268 t.icon_pos_base = v;
1269 }
1270 let icon_pos_raw = raw_top("ICON_POS");
1271 let icon_pos = parse_i64_tuple(
1272 icon_pos_raw
1273 .as_deref()
1274 .or_else(|| cfg.get_indexed_field("WAKU", i, "ICON_POS")),
1275 );
1276 if icon_pos.len() >= 3 {
1277 t.icon_pos_base = icon_pos[0];
1280 t.icon_pos = (icon_pos[1], icon_pos[2], 0);
1281 }
1282
1283 for btn_idx in 0..t.buttons.len() {
1284 let mut b = WakuButtonTemplate::default();
1285
1286 let file_raw = raw_nested_indexed_field(&raw_fields, "WAKU", i, "BTN", btn_idx, "FILE");
1287 if let Some(v) = file_raw
1288 .as_deref()
1289 .or_else(|| nested_indexed_field_unquoted(cfg, "WAKU", i, "BTN", btn_idx, "FILE"))
1290 {
1291 b.file_name = trim_gameexe_scalar(v).to_string();
1292 }
1293
1294 let cut_raw =
1295 raw_nested_indexed_field(&raw_fields, "WAKU", i, "BTN", btn_idx, "CUT_NO");
1296 if let Some(v) = cut_raw
1297 .as_deref()
1298 .or_else(|| nested_indexed_field(cfg, "WAKU", i, "BTN", btn_idx, "CUT_NO"))
1299 .and_then(parse_i64_like_local)
1300 {
1301 b.cut_no = v;
1302 }
1303
1304 let pos_raw = raw_nested_indexed_field(&raw_fields, "WAKU", i, "BTN", btn_idx, "POS");
1305 let pos = parse_i64_tuple(
1306 pos_raw
1307 .as_deref()
1308 .or_else(|| nested_indexed_field(cfg, "WAKU", i, "BTN", btn_idx, "POS")),
1309 );
1310 if pos.len() >= 3 {
1311 b.pos_base = pos[0];
1314 b.pos = (pos[1], pos[2]);
1315 }
1316
1317 let action_raw =
1318 raw_nested_indexed_field(&raw_fields, "WAKU", i, "BTN", btn_idx, "ACTION");
1319 if let Some(v) = action_raw
1320 .as_deref()
1321 .or_else(|| nested_indexed_field(cfg, "WAKU", i, "BTN", btn_idx, "ACTION"))
1322 .and_then(parse_i64_like_local)
1323 {
1324 b.action_no = v;
1325 }
1326
1327 let se_raw = raw_nested_indexed_field(&raw_fields, "WAKU", i, "BTN", btn_idx, "SE");
1328 if let Some(v) = se_raw
1329 .as_deref()
1330 .or_else(|| nested_indexed_field(cfg, "WAKU", i, "BTN", btn_idx, "SE"))
1331 .and_then(parse_i64_like_local)
1332 {
1333 b.se_no = v;
1334 }
1335
1336 let type_raw = raw_nested_indexed_field(&raw_fields, "WAKU", i, "BTN", btn_idx, "TYPE");
1337 if let Some(v) = type_raw
1338 .as_deref()
1339 .or_else(|| nested_indexed_field_unquoted(cfg, "WAKU", i, "BTN", btn_idx, "TYPE"))
1340 {
1341 parse_waku_button_type(v, &mut b);
1342 }
1343
1344 let call_raw = raw_nested_indexed_field(&raw_fields, "WAKU", i, "BTN", btn_idx, "CALL");
1345 if let Some(v) = call_raw
1346 .as_deref()
1347 .or_else(|| nested_indexed_field_unquoted(cfg, "WAKU", i, "BTN", btn_idx, "CALL"))
1348 {
1349 let parts: Vec<&str> = v.split(',').map(|p| p.trim().trim_matches('"')).collect();
1350 if parts.len() >= 2 {
1351 b.scn_name = parts[0].to_string();
1352 if let Some(z) = parse_i64_like_local(parts[1]) {
1353 b.z_no = z;
1354 } else {
1355 b.cmd_name = parts[1].to_string();
1356 }
1357 }
1358 }
1359
1360 let frame_action_raw =
1361 raw_nested_indexed_field(&raw_fields, "WAKU", i, "BTN", btn_idx, "FRAME_ACTION");
1362 if let Some(v) = frame_action_raw.as_deref().or_else(|| {
1363 nested_indexed_field_unquoted(cfg, "WAKU", i, "BTN", btn_idx, "FRAME_ACTION")
1364 }) {
1365 let parts: Vec<&str> = v.split(',').map(|p| p.trim().trim_matches('"')).collect();
1366 if parts.len() >= 2 {
1367 b.frame_action_scn_name = parts[0].to_string();
1368 b.frame_action_cmd_name = parts[1].to_string();
1369 }
1370 }
1371
1372 if !b.file_name.is_empty()
1373 || b.cut_no != 0
1374 || b.pos != (0, 0)
1375 || b.action_no != 0
1376 || b.se_no != 0
1377 {
1378 t.buttons[btn_idx] = b;
1379 }
1380 }
1381
1382 for face_idx in 0..t.face_pos.len() {
1383 let pos_raw = raw_nested_indexed_field(&raw_fields, "WAKU", i, "FACE", face_idx, "POS");
1384 let pos = parse_i64_tuple(
1385 pos_raw
1386 .as_deref()
1387 .or_else(|| nested_indexed_field(cfg, "WAKU", i, "FACE", face_idx, "POS")),
1388 );
1389 if pos.len() >= 2 {
1390 t.face_pos[face_idx] = (pos[0], pos[1]);
1391 }
1392 }
1393
1394 out[i] = t;
1395 }
1396
1397 out
1398}
1399
1400fn default_color_table() -> Vec<(u8, u8, u8)> {
1401 let mut out = vec![(255, 255, 255); 256];
1402 out[0] = (255, 255, 255);
1403 out[1] = (0, 0, 0);
1404 out[2] = (255, 0, 0);
1405 out[3] = (0, 255, 0);
1406 out[4] = (0, 0, 255);
1407 out[5] = (255, 255, 0);
1408 out[6] = (255, 0, 255);
1409 out[7] = (0, 255, 255);
1410 out
1411}
1412
1413fn load_color_table(cfg: &GameexeConfig) -> Vec<(u8, u8, u8)> {
1414 let cnt = cfg
1415 .get_usize("COLOR_TABLE.CNT")
1416 .unwrap_or(256)
1417 .clamp(1, 4096);
1418 let mut out = default_color_table();
1419 if out.len() < cnt {
1420 out.resize(cnt, (255, 255, 255));
1421 }
1422
1423 for i in 0..cnt {
1424 let Some(raw) = cfg
1425 .get_indexed_value("COLOR_TABLE", i)
1426 .or_else(|| cfg.get_indexed_field("COLOR_TABLE", i, "RGB"))
1427 .or_else(|| cfg.get_indexed_field("COLOR_TABLE", i, "COLOR"))
1428 else {
1429 continue;
1430 };
1431 let vals = parse_i64_tuple(Some(raw));
1432 if vals.len() >= 3 {
1433 out[i] = (
1434 vals[0].clamp(0, 255) as u8,
1435 vals[1].clamp(0, 255) as u8,
1436 vals[2].clamp(0, 255) as u8,
1437 );
1438 }
1439 }
1440 out
1441}
1442
1443fn load_font_config_defaults(cfg: &GameexeConfig) -> FontConfigDefaults {
1444 FontConfigDefaults {
1445 font_type: cfg
1446 .get_unquoted("CONFIG.FONT.TYPE")
1447 .and_then(parse_i64_like_local)
1448 .unwrap_or(0),
1449 futoku: cfg
1450 .get_unquoted("CONFIG.FONT.FUTOKU")
1451 .and_then(parse_i64_like_local)
1452 .unwrap_or(0),
1453 shadow: cfg
1454 .get_unquoted("CONFIG.FONT.SHADOW")
1455 .and_then(parse_i64_like_local)
1456 .unwrap_or(0),
1457 }
1458}
1459
1460fn load_namae_entries(raw_text: Option<&str>) -> Vec<NamaeEntry> {
1461 let Some(text) = raw_text else {
1462 return Vec::new();
1463 };
1464 let mut out = Vec::new();
1465 for line in text.lines() {
1466 let mut t = line.trim();
1467 if t.is_empty() {
1468 continue;
1469 }
1470 if let Some(rest) = t.strip_prefix('#') {
1471 t = rest.trim_start();
1472 }
1473 let Some(rhs) = t.strip_prefix("NAMAE") else {
1474 continue;
1475 };
1476 let Some(rhs) = rhs.trim_start().strip_prefix('=') else {
1477 continue;
1478 };
1479 let fields = split_gameexe_fields(rhs);
1480 if fields.len() < 5 {
1481 continue;
1482 }
1483 let source = trim_gameexe_scalar(&fields[0]).to_string();
1484 let display = trim_gameexe_scalar(&fields[1]).to_string();
1485 if source.is_empty() {
1486 continue;
1487 }
1488 let color_mod = fields
1489 .get(2)
1490 .and_then(|v| parse_i64_like_local(v))
1491 .unwrap_or(0);
1492 let moji_color_no = fields
1493 .get(3)
1494 .and_then(|v| parse_i64_like_local(v))
1495 .unwrap_or(-1);
1496 let shadow_color_no = fields
1497 .get(4)
1498 .and_then(|v| parse_i64_like_local(v))
1499 .unwrap_or(-1);
1500 let fuchi_color_no = fields
1501 .get(5)
1502 .and_then(|v| parse_i64_like_local(v))
1503 .unwrap_or(-1);
1504 out.push(NamaeEntry {
1505 source,
1506 display,
1507 color_mod,
1508 moji_color_no,
1509 shadow_color_no,
1510 fuchi_color_no,
1511 });
1512 }
1513 out
1514}
1515
1516fn split_gameexe_fields(raw: &str) -> Vec<String> {
1517 let mut fields = Vec::new();
1518 let mut cur = String::new();
1519 let mut in_quote = false;
1520 let mut paren_depth = 0i32;
1521 let mut chars = raw.chars().peekable();
1522 for ch in chars {
1523 match ch {
1524 '"' => {
1525 in_quote = !in_quote;
1526 cur.push(ch);
1527 }
1528 '(' if !in_quote => {
1529 paren_depth += 1;
1530 cur.push(ch);
1531 }
1532 ')' if !in_quote => {
1533 paren_depth = (paren_depth - 1).max(0);
1534 cur.push(ch);
1535 }
1536 ',' if !in_quote && paren_depth == 0 => {
1537 fields.push(cur.trim().to_string());
1538 cur.clear();
1539 }
1540 _ => cur.push(ch),
1541 }
1542 }
1543 if !cur.trim().is_empty() {
1544 fields.push(cur.trim().to_string());
1545 }
1546 fields
1547}
1548
1549fn load_icon_templates(cfg: &GameexeConfig) -> Vec<IconTemplate> {
1550 let cnt = cfg
1551 .get_usize("ICON.CNT")
1552 .unwrap_or(INIDEF_ICON_CNT)
1553 .min(INIMAX_ICON_CNT);
1554 let mut out = vec![IconTemplate::default(); cnt];
1555
1556 for i in 0..cnt {
1557 let mut t = IconTemplate::default();
1558 if let Some(v) = cfg
1559 .get_indexed_field_unquoted("ICON", i, "FILE_NAME")
1560 .or_else(|| cfg.get_indexed_field_unquoted("ICON", i, "FILE"))
1561 .or_else(|| cfg.get_indexed_unquoted("ICON", i))
1562 {
1563 t.file_name = v.to_string();
1564 }
1565 if let Some(v) = cfg
1566 .get_indexed_field("ICON", i, "ANIME_PAT_CNT")
1567 .or_else(|| cfg.get_indexed_field("ICON", i, "CNT"))
1568 .or_else(|| cfg.get_indexed_field("ICON", i, "PAT_CNT"))
1569 .and_then(parse_i64_like_local)
1570 {
1571 t.anime_pat_cnt = v.max(1);
1572 }
1573 if let Some(v) = cfg
1574 .get_indexed_field("ICON", i, "ANIME_SPEED")
1575 .or_else(|| cfg.get_indexed_field("ICON", i, "SPEED"))
1576 .and_then(parse_i64_like_local)
1577 {
1578 t.anime_speed = v.max(1);
1579 }
1580 out[i] = t;
1581 }
1582
1583 out
1584}
1585
1586fn load_sel_btn_templates(cfg: &GameexeConfig) -> Vec<SelBtnTemplate> {
1587 let cnt = cfg
1588 .get_usize("SELBTN.CNT")
1589 .unwrap_or(INIDEF_SEL_BTN_CNT)
1590 .clamp(INIMIN_SEL_BTN_CNT, INIMAX_SEL_BTN_CNT);
1591 let mut out = vec![SelBtnTemplate::default(); cnt];
1592 for i in 0..cnt {
1593 let mut t = SelBtnTemplate::default();
1594 let get_i64 = |field: &str| {
1595 cfg.get_indexed_field("SELBTN", i, field)
1596 .and_then(parse_i64_like_local)
1597 };
1598 let get_tuple = |field: &str| parse_i64_tuple(cfg.get_indexed_field("SELBTN", i, field));
1599
1600 if let Some(v) = cfg.get_indexed_field_unquoted("SELBTN", i, "BASE_FILE") {
1601 t.base_file = v.to_string();
1602 }
1603 if let Some(v) = cfg.get_indexed_field_unquoted("SELBTN", i, "BACK_FILE") {
1604 t.filter_file = v.to_string();
1605 }
1606 let base_pos = get_tuple("BASE_POS");
1607 if base_pos.len() >= 2 {
1608 t.base_pos = (base_pos[0], base_pos[1]);
1609 }
1610 let rep_pos = get_tuple("REP_POS");
1611 if rep_pos.len() >= 2 {
1612 t.rep_pos = (rep_pos[0], rep_pos[1]);
1613 }
1614 let align = get_tuple("ALIGN");
1615 if align.len() >= 2 {
1616 t.x_align = align[0];
1617 t.y_align = align[1];
1618 }
1619 if let Some(v) = get_i64("MAX_Y_CNT") {
1620 t.max_y_cnt = v;
1621 }
1622 if let Some(v) = get_i64("LINE_WIDTH") {
1623 t.line_width = v;
1624 }
1625 let moji_size = get_tuple("MOJI_SIZE");
1626 if moji_size.len() >= 4 {
1627 t.moji_size = moji_size[0];
1628 t.moji_space = (moji_size[1], moji_size[2]);
1629 t.moji_cnt = moji_size[3];
1630 }
1631 let moji_pos = get_tuple("MOJI_POS");
1632 if moji_pos.len() >= 2 {
1633 t.moji_pos = (moji_pos[0], moji_pos[1]);
1634 }
1635 let moji_align = get_tuple("MOJI_ALIGN");
1636 if moji_align.len() >= 2 {
1637 t.moji_x_align = moji_align[0];
1638 t.moji_y_align = moji_align[1];
1639 }
1640 if let Some(v) = get_i64("MOJI_COLOR") {
1641 t.moji_color = v;
1642 }
1643 if let Some(v) = get_i64("MOJI_HIT_COLOR") {
1644 t.moji_hit_color = v;
1645 }
1646 if let Some(v) = get_i64("BTN_ACTION") {
1647 t.btn_action_no = v;
1648 }
1649 let open_anime = get_tuple("OPEN_ANIME");
1650 if open_anime.len() >= 2 {
1651 t.open_anime_type = open_anime[0];
1652 t.open_anime_time = open_anime[1];
1653 }
1654 let close_anime = get_tuple("CLOSE_ANIME");
1655 if close_anime.len() >= 2 {
1656 t.close_anime_type = close_anime[0];
1657 t.close_anime_time = close_anime[1];
1658 }
1659 let decide_anime = get_tuple("DECIDE_ANIME");
1660 if decide_anime.len() >= 2 {
1661 t.decide_anime_type = decide_anime[0];
1662 t.decide_anime_time = decide_anime[1];
1663 }
1664 out[i] = t;
1665 }
1666 out
1667}
1668
1669fn load_emoji_templates(cfg: &GameexeConfig) -> Vec<EmojiTemplate> {
1670 let cnt = cfg.get_usize("EMOJI.CNT").unwrap_or(16).min(256);
1671 let mut out = vec![EmojiTemplate::default(); cnt];
1672 for (i, item) in out.iter_mut().enumerate() {
1673 let raw = cfg.get_unquoted(&format!("EMOJI.{i}"));
1674 let Some(raw) = raw else {
1675 continue;
1676 };
1677 let mut parts = raw.split(',').map(str::trim);
1678 item.file_name = parts.next().unwrap_or("").trim_matches('"').to_string();
1679 item.font_size = parts.next().and_then(parse_i64_like_local).unwrap_or(0);
1680 }
1681 out
1682}
1683
1684fn load_message_button_templates(cfg: &GameexeConfig) -> Vec<MessageButtonTemplate> {
1685 let cnt = cfg.get_usize("MSGBTN.CNT").unwrap_or(16).min(256);
1686 let mut out = vec![MessageButtonTemplate::default(); cnt];
1687 const STATES: [(&str, usize); 3] = [("NORMAL", 0), ("HIT", 1), ("PUSH", 2)];
1688 for (i, item) in out.iter_mut().enumerate() {
1689 for (name, state) in STATES {
1690 let key = format!("MSGBTN.{i}.{name}.COLOR");
1691 if let Some(value) = cfg.get_unquoted(&key).and_then(parse_i64_like_local) {
1692 item.color_no[state] = value;
1693 }
1694 }
1695 }
1696 out
1697}
1698
1699fn load_button_action_templates(cfg: &GameexeConfig) -> Vec<ButtonActionTemplate> {
1700 let cnt = cfg
1701 .get_usize("BUTTON.ACTION.CNT")
1702 .unwrap_or(INIDEF_BTN_ACTION_CNT)
1703 .min(INIMAX_BTN_ACTION_CNT);
1704 let mut out = vec![ButtonActionTemplate::default(); cnt];
1705
1706 const STATES: [(&str, usize); TNM_BTN_STATE_MAX] = [
1707 ("NORMAL", 0),
1708 ("HIT", 1),
1709 ("PUSH", 2),
1710 ("SELECT", 3),
1711 ("DISABLE", 4),
1712 ];
1713
1714 for i in 0..cnt {
1715 for (name, state_idx) in STATES {
1716 let Some(raw) = cfg.get_indexed_field("BUTTON.ACTION", i, name) else {
1717 continue;
1718 };
1719 let nums = parse_button_action_numbers(raw);
1720 if nums.len() >= 6 {
1721 out[i].state[state_idx] = ButtonActionPattern {
1722 rep_pat_no: nums[0],
1723 rep_pos_x: nums[1],
1724 rep_pos_y: nums[2],
1725 rep_tr: nums[3].clamp(0, 255),
1726 rep_bright: nums[4].clamp(0, 255),
1727 rep_dark: nums[5].clamp(0, 255),
1728 };
1729 }
1730 }
1731 }
1732
1733 out
1734}
1735
1736fn parse_button_action_numbers(raw: &str) -> Vec<i64> {
1737 raw.split(|c: char| c == ',' || c.is_ascii_whitespace())
1738 .filter(|s| !s.trim().is_empty())
1739 .filter_map(parse_i64_like_local)
1740 .collect()
1741}
1742
1743fn parse_signed_decimal_numbers(raw: &str) -> Vec<i64> {
1744 let bytes = raw.as_bytes();
1745 let mut out = Vec::new();
1746 let mut i = 0usize;
1747 while i < bytes.len() {
1748 let signed = bytes[i] == b'-' || bytes[i] == b'+';
1749 let digit_at = if signed { i + 1 } else { i };
1750 if digit_at >= bytes.len() || !bytes[digit_at].is_ascii_digit() {
1751 i += 1;
1752 continue;
1753 }
1754 let start = i;
1755 i = digit_at + 1;
1756 while i < bytes.len() && bytes[i].is_ascii_digit() {
1757 i += 1;
1758 }
1759 if let Ok(v) = raw[start..i].parse::<i64>() {
1760 out.push(v);
1761 }
1762 }
1763 out
1764}
1765
1766fn load_shake_templates(cfg: &GameexeConfig) -> Vec<Vec<ShakeStep>> {
1767 let cnt = cfg
1768 .get_usize("SHAKE.CNT")
1769 .unwrap_or(INIDEF_SHAKE_CNT)
1770 .min(INIMAX_SHAKE_CNT);
1771 let mut out = vec![Vec::new(); cnt];
1772 for (i, steps) in out.iter_mut().enumerate() {
1773 let Some(entry) = cfg.get_indexed_entry("SHAKE", i) else {
1774 continue;
1775 };
1776 let nums = parse_signed_decimal_numbers(&entry.value);
1777 for triple in nums.as_chunks::<3>().0.iter() {
1778 steps.push(ShakeStep {
1779 x: triple[0].clamp(i32::MIN as i64, i32::MAX as i64) as i32,
1780 y: triple[1].clamp(i32::MIN as i64, i32::MAX as i64) as i32,
1781 time_ms: triple[2].clamp(0, i32::MAX as i64) as i32,
1782 });
1783 }
1784 }
1785 out
1786}
1787
1788fn load_se_file_names(cfg: &GameexeConfig) -> Vec<Option<String>> {
1789 let cnt = cfg
1790 .get_usize("SE.CNT")
1791 .unwrap_or(INIDEF_SE_CNT)
1792 .clamp(INIMIN_SE_CNT, INIMAX_SE_CNT);
1793 let mut out = vec![None; cnt];
1794
1795 for i in 0..cnt {
1796 if let Some(v) = cfg.get_indexed_unquoted("SE", i) {
1797 let t = v.trim();
1798 if !t.is_empty() {
1799 out[i] = Some(t.to_string());
1800 }
1801 }
1802 }
1803
1804 out
1805}
1806
1807fn load_button_se_templates(cfg: &GameexeConfig) -> Vec<ButtonSeTemplate> {
1808 let cnt = cfg
1809 .get_usize("BUTTON.SE.CNT")
1810 .unwrap_or(INIDEF_BTN_SE_CNT)
1811 .min(INIMAX_BTN_SE_CNT);
1812 let mut out = vec![ButtonSeTemplate::default(); cnt];
1813
1814 for i in 0..cnt {
1815 if let Some(v) = cfg
1816 .get_indexed_field("BUTTON.SE", i, "HIT")
1817 .and_then(parse_i64_like_local)
1818 {
1819 out[i].hit_no = v;
1820 }
1821 if let Some(v) = cfg
1822 .get_indexed_field("BUTTON.SE", i, "PUSH")
1823 .and_then(parse_i64_like_local)
1824 {
1825 out[i].push_no = v;
1826 }
1827 if let Some(v) = cfg
1828 .get_indexed_field("BUTTON.SE", i, "DECIDE")
1829 .and_then(parse_i64_like_local)
1830 {
1831 out[i].decide_no = v;
1832 }
1833 }
1834
1835 out
1836}
1837
1838fn parse_i64_like_local(s: &str) -> Option<i64> {
1839 let t = s.trim().trim_matches('"');
1840 if let Some(hex) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
1841 i64::from_str_radix(hex, 16).ok()
1842 } else {
1843 t.parse::<i64>().ok()
1844 }
1845}
1846
1847fn path_is_file(path: &Path) -> bool {
1848 crate::resource::game_file_exists(path)
1849}
1850
1851fn path_is_dir(path: &Path) -> bool {
1852 crate::resource::resolve_game_path(path)
1853 .ok()
1854 .flatten()
1855 .is_some_and(|p| {
1856 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1857 {
1858 crate::resource::wasm_path_is_dir(&p)
1859 }
1860 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
1861 {
1862 p.is_dir()
1863 }
1864 })
1865}
1866
1867fn load_key_toml_config(
1868 project_dir: &Path,
1869) -> anyhow::Result<Option<siglus_assets::key_toml::KeyTomlConfig>> {
1870 crate::resource::load_project_key_toml(project_dir)
1871}
1872
1873fn load_gameexe_decode_options(project_dir: &Path) -> anyhow::Result<GameexeDecodeOptions> {
1874 crate::resource::load_gameexe_decode_options(project_dir)
1875}
1876
1877fn resolve_table_path(
1878 project_dir: &Path,
1879 dat_dir: &Path,
1880 raw: &str,
1881 default_ext: Option<&str>,
1882) -> Option<PathBuf> {
1883 let s = raw.trim().trim_matches('"');
1884 if s.is_empty() {
1885 return None;
1886 }
1887
1888 let normalized = s.replace('\\', "/");
1889 let mut candidates = Vec::new();
1890 candidates.push(PathBuf::from(&normalized));
1891
1892 let mut with_ext = PathBuf::from(&normalized);
1893 if with_ext.extension().is_none()
1894 && let Some(ext) = default_ext
1895 {
1896 with_ext.set_extension(ext);
1897 }
1898 if with_ext != normalized {
1899 candidates.push(with_ext.clone());
1900 }
1901
1902 for cand in candidates {
1903 let direct = if cand.is_absolute() {
1904 cand.clone()
1905 } else {
1906 project_dir.join(&cand)
1907 };
1908 if let Some(resolved) = crate::resource::resolve_game_file(&direct).ok().flatten() {
1909 return Some(resolved);
1910 }
1911
1912 let file_name = cand.file_name().map(PathBuf::from);
1913 if let Some(name_only) = file_name {
1914 let p = dat_dir.join(&name_only);
1915 if let Some(resolved) = crate::resource::resolve_game_file(&p).ok().flatten() {
1916 return Some(resolved);
1917 }
1918 }
1919
1920 let p = dat_dir.join(&cand);
1921 if let Some(resolved) = crate::resource::resolve_game_file(&p).ok().flatten() {
1922 return Some(resolved);
1923 }
1924
1925 if let Ok(stripped) = cand.strip_prefix("dat") {
1926 let p = dat_dir.join(stripped);
1927 if let Some(resolved) = crate::resource::resolve_game_file(&p).ok().flatten() {
1928 return Some(resolved);
1929 }
1930 }
1931 }
1932 None
1933}
1934
1935#[cfg(test)]
1936mod raw_gameexe_tests {
1937 use super::*;
1938
1939 #[test]
1940 fn raw_index_matches_original_last_definition_wins_and_keeps_quoting() {
1941 let text = "\u{feff} # waku . 000 . waku_file = \"first\" ;\n\
1942 #WAKU.000.WAKU_FILE = \"last,b=c\"\n\
1943 malformed line\n";
1944 let fields = index_raw_gameexe_fields(Some(text));
1945 assert_eq!(
1946 raw_indexed_field(&fields, "WAKU", 0, "WAKU_FILE").as_deref(),
1947 Some("\"last,b=c\"")
1948 );
1949 assert!(raw_indexed_field(&fields, "WAKU", 1, "WAKU_FILE").is_none());
1950 assert!(index_raw_gameexe_fields(None).is_empty());
1951 }
1952
1953 #[test]
1954 fn raw_index_canonicalizes_numeric_components_and_last_definition_wins() {
1955 let fields = index_raw_gameexe_fields(Some(
1956 "#WAKU.1.BTN.2.FILE = \"plain first\"\n\
1957 #WAKU.001.BTN.002.FILE = \"padded last\"\n\
1958 #WAKU.001.BTN.003.FILE = \"only padded\"\n",
1959 ));
1960 assert_eq!(
1961 raw_nested_indexed_field(&fields, "WAKU", 1, "BTN", 2, "FILE").as_deref(),
1962 Some("\"padded last\"")
1963 );
1964 assert_eq!(
1965 raw_nested_indexed_field(&fields, "WAKU", 1, "BTN", 3, "FILE").as_deref(),
1966 Some("\"only padded\"")
1967 );
1968 }
1969}