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