1mod file_sink;
2
3use self::file_sink::FileSink;
4use crate::util::ArrayLikeElement;
5use crate::GameKind;
6use anyhow::bail;
7use anyhow::ensure;
8use anyhow::Context;
9use rpgmxp_types::Actor;
10use rpgmxp_types::Animation;
11use rpgmxp_types::Armor;
12use rpgmxp_types::Class;
13use rpgmxp_types::CommonEvent;
14use rpgmxp_types::Enemy;
15use rpgmxp_types::Item;
16use rpgmxp_types::Script;
17use rpgmxp_types::ScriptList;
18use rpgmxp_types::Skill;
19use rpgmxp_types::State;
20use rpgmxp_types::Tileset;
21use rpgmxp_types::Troop;
22use rpgmxp_types::Weapon;
23use ruby_marshal::IntoValue;
24use std::collections::BTreeMap;
25use std::fs::File;
26use std::path::Component as PathComponent;
27use std::path::Path;
28use std::path::PathBuf;
29use std::str::FromStr;
30use walkdir::WalkDir;
31
32#[derive(Debug)]
33enum Format {
34 Dir,
35 Rgssad,
36 Rgss2a,
37}
38
39impl FromStr for Format {
40 type Err = anyhow::Error;
41
42 fn from_str(input: &str) -> Result<Self, Self::Err> {
43 match input {
44 "dir" => Ok(Self::Dir),
45 "rgssad" => Ok(Self::Rgssad),
46 "rgss2a" => Ok(Self::Rgss2a),
47 _ => bail!("unknown format \"{input}\""),
48 }
49 }
50}
51
52#[derive(Debug, argh::FromArgs)]
53#[argh(
54 subcommand,
55 name = "compile-assets",
56 description = "recompile extracted assets from a folder"
57)]
58pub struct Options {
59 #[argh(positional, description = "the input folder path to compile")]
60 input: PathBuf,
61
62 #[argh(positional, description = "the output path")]
63 output: PathBuf,
64
65 #[argh(
66 option,
67 long = "format",
68 short = 'f',
69 description = "the output format. Defaults to detecting from the extension. Otherwise, \"dir\" is used."
70 )]
71 format: Option<Format>,
72
73 #[argh(
74 option,
75 long = "game",
76 short = 'g',
77 description = "the game type. Defaults to detecting from the output format. Must be provided if the output format is a dir."
78 )]
79 game: Option<GameKind>,
80
81 #[argh(
82 switch,
83 long = "overwrite",
84 description = "whether overwrite the output if it exists"
85 )]
86 pub overwrite: bool,
87}
88
89pub fn exec(mut options: Options) -> anyhow::Result<()> {
90 options.input = options
91 .input
92 .canonicalize()
93 .context("failed to canonicalize input path")?;
94
95 let format = match options.format {
96 Some(format) => format,
97 None => {
98 let extension = options
99 .output
100 .extension()
101 .map(|extension| extension.to_str().context("non-unicode extension"))
102 .transpose()?;
103 if extension == Some("rgssad") {
104 Format::Rgssad
105 } else if extension == Some("rgss2a") {
106 Format::Rgss2a
107 } else {
108 Format::Dir
109 }
110 }
111 };
112
113 let mut file_sink = match format {
114 Format::Dir => FileSink::new_dir(&options.output, options.overwrite)?,
115 Format::Rgssad | Format::Rgss2a => {
116 FileSink::new_rgssad(&options.output, options.overwrite)?
117 }
118 };
119 let game_kind = options.game.map(Ok).unwrap_or_else(|| match format {
120 Format::Dir => {
121 bail!("need to provide game type with --game flag when outputting to a dir.")
122 }
123 Format::Rgssad => Ok(GameKind::Xp),
124 Format::Rgss2a => Ok(GameKind::Vx),
125 })?;
126
127 for entry in WalkDir::new(&options.input) {
128 let entry = entry?;
129 let entry_file_type = entry.file_type();
130 let entry_path = entry.path();
131
132 let relative_path = entry_path.strip_prefix(&options.input)?;
133 let relative_path_components = relative_path
134 .components()
135 .map(|component| match component {
136 PathComponent::Normal(value) => value.to_str().context("non-unicode path"),
137 component => bail!("unexpected path component \"{component:?}\""),
138 })
139 .collect::<anyhow::Result<Vec<_>>>()?;
140
141 match game_kind {
142 GameKind::Xp => compile_xp(
143 entry_path,
144 entry_file_type,
145 relative_path,
146 relative_path_components,
147 &mut file_sink,
148 )?,
149 GameKind::Vx => compile_vx(
150 entry_path,
151 entry_file_type,
152 relative_path,
153 relative_path_components,
154 &mut file_sink,
155 )?,
156 }
157 }
158
159 file_sink.finish()?;
160
161 Ok(())
162}
163
164fn compile_xp(
165 entry_path: &Path,
166 entry_file_type: std::fs::FileType,
167 relative_path: &Path,
168 relative_path_components: Vec<&str>,
169 file_sink: &mut FileSink,
170) -> anyhow::Result<()> {
171 match relative_path_components.as_slice() {
172 ["Data", "Scripts.rxdata"] if entry_file_type.is_dir() => {
173 println!("packing \"{}\"", relative_path.display());
174
175 let scripts_data = generate_scripts_data(entry_path)?;
176 let size = u32::try_from(scripts_data.len())?;
177
178 file_sink.write_file(&relative_path_components, size, &*scripts_data)?;
179 }
180 ["Data", "Scripts.rxdata", ..] => {
181 }
183 ["Data", "CommonEvents.rxdata"] if entry_file_type.is_dir() => {
184 println!("packing \"{}\"", relative_path.display());
185
186 let rx_data = generate_arraylike_rx_data::<CommonEvent>(entry_path)?;
187 let size = u32::try_from(rx_data.len())?;
188
189 file_sink.write_file(&relative_path_components, size, &*rx_data)?;
190 }
191 ["Data", "CommonEvents.rxdata", ..] => {
192 }
194 ["Data", "Actors.rxdata"] if entry_file_type.is_dir() => {
195 println!("packing \"{}\"", relative_path.display());
196
197 let rx_data = generate_arraylike_rx_data::<Actor>(entry_path)?;
198 let size = u32::try_from(rx_data.len())?;
199
200 file_sink.write_file(&relative_path_components, size, &*rx_data)?;
201 }
202 ["Data", "Actors.rxdata", ..] => {
203 }
205 ["Data", "Weapons.rxdata"] if entry_file_type.is_dir() => {
206 println!("packing \"{}\"", relative_path.display());
207
208 let rx_data = generate_arraylike_rx_data::<Weapon>(entry_path)?;
209 let size = u32::try_from(rx_data.len())?;
210
211 file_sink.write_file(&relative_path_components, size, &*rx_data)?;
212 }
213 ["Data", "Weapons.rxdata", ..] => {
214 }
216 ["Data", "Armors.rxdata"] if entry_file_type.is_dir() => {
217 println!("packing \"{}\"", relative_path.display());
218
219 let rx_data = generate_arraylike_rx_data::<Armor>(entry_path)?;
220 let size = u32::try_from(rx_data.len())?;
221
222 file_sink.write_file(&relative_path_components, size, &*rx_data)?;
223 }
224 ["Data", "Armors.rxdata", ..] => {
225 }
227 ["Data", "Skills.rxdata"] if entry_file_type.is_dir() => {
228 println!("packing \"{}\"", relative_path.display());
229
230 let rx_data = generate_arraylike_rx_data::<Skill>(entry_path)?;
231 let size = u32::try_from(rx_data.len())?;
232
233 file_sink.write_file(&relative_path_components, size, &*rx_data)?;
234 }
235 ["Data", "Skills.rxdata", ..] => {
236 }
238 ["Data", "States.rxdata"] if entry_file_type.is_dir() => {
239 println!("packing \"{}\"", relative_path.display());
240
241 let rx_data = generate_arraylike_rx_data::<State>(entry_path)?;
242 let size = u32::try_from(rx_data.len())?;
243
244 file_sink.write_file(&relative_path_components, size, &*rx_data)?;
245 }
246 ["Data", "States.rxdata", ..] => {
247 }
249 ["Data", "Items.rxdata"] if entry_file_type.is_dir() => {
250 println!("packing \"{}\"", relative_path.display());
251
252 let rx_data = generate_arraylike_rx_data::<Item>(entry_path)?;
253 let size = u32::try_from(rx_data.len())?;
254
255 file_sink.write_file(&relative_path_components, size, &*rx_data)?;
256 }
257 ["Data", "Items.rxdata", ..] => {
258 }
260 ["Data", "Enemies.rxdata"] if entry_file_type.is_dir() => {
261 println!("packing \"{}\"", relative_path.display());
262
263 let rx_data = generate_arraylike_rx_data::<Enemy>(entry_path)?;
264 let size = u32::try_from(rx_data.len())?;
265
266 file_sink.write_file(&relative_path_components, size, &*rx_data)?;
267 }
268 ["Data", "Enemies.rxdata", ..] => {
269 }
271 ["Data", "Classes.rxdata"] if entry_file_type.is_dir() => {
272 println!("packing \"{}\"", relative_path.display());
273
274 let rx_data = generate_arraylike_rx_data::<Class>(entry_path)?;
275 let size = u32::try_from(rx_data.len())?;
276
277 file_sink.write_file(&relative_path_components, size, &*rx_data)?;
278 }
279 ["Data", "Classes.rxdata", ..] => {
280 }
282 ["Data", "Troops.rxdata"] if entry_file_type.is_dir() => {
283 println!("packing \"{}\"", relative_path.display());
284
285 let rx_data = generate_arraylike_rx_data::<Troop>(entry_path)?;
286 let size = u32::try_from(rx_data.len())?;
287
288 file_sink.write_file(&relative_path_components, size, &*rx_data)?;
289 }
290 ["Data", "Troops.rxdata", ..] => {
291 }
293 ["Data", "Tilesets.rxdata"] if entry_file_type.is_dir() => {
294 println!("packing \"{}\"", relative_path.display());
295
296 let rx_data = generate_arraylike_rx_data::<Tileset>(entry_path)?;
297 let size = u32::try_from(rx_data.len())?;
298
299 file_sink.write_file(&relative_path_components, size, &*rx_data)?;
300 }
301 ["Data", "Tilesets.rxdata", ..] => {
302 }
304 ["Data", "MapInfos.rxdata"] if entry_file_type.is_dir() => {
305 println!("packing \"{}\"", relative_path.display());
306
307 let rx_data = generate_map_infos_data(entry_path)?;
308 let size = u32::try_from(rx_data.len())?;
309
310 file_sink.write_file(&relative_path_components, size, &*rx_data)?;
311 }
312 ["Data", "MapInfos.rxdata", ..] => {
313 }
315 ["Data", "System.json"] if entry_file_type.is_file() => {
316 println!("packing \"{}\"", relative_path.display());
317
318 let data = generate_ruby_data::<rpgmxp_types::System>(entry_path)?;
319 let size = u32::try_from(data.len())?;
320
321 let mut relative_path_components = relative_path_components.clone();
322 *relative_path_components.last_mut().unwrap() = "System.rxdata";
323
324 file_sink.write_file(&relative_path_components, size, &*data)?;
325 }
326 ["Data", "Animations.rxdata"] if entry_file_type.is_dir() => {
327 println!("packing \"{}\"", relative_path.display());
328
329 let rx_data = generate_arraylike_rx_data::<Animation>(entry_path)?;
330 let size = u32::try_from(rx_data.len())?;
331
332 file_sink.write_file(&relative_path_components, size, &*rx_data)?;
333 }
334 ["Data", "Animations.rxdata", ..] => {
335 }
337 ["Data", file] if crate::util::is_map_file_name(file, "json") => {
338 println!("packing \"{}\"", relative_path.display());
339
340 let map_data = generate_ruby_data::<rpgmxp_types::Map>(entry_path)?;
341 let size = u32::try_from(map_data.len())?;
342
343 let renamed_file = set_extension_str(file, "rxdata");
344 let mut relative_path_components = relative_path_components.clone();
345 *relative_path_components.last_mut().unwrap() = renamed_file.as_str();
346
347 file_sink.write_file(&relative_path_components, size, &*map_data)?;
348 }
349 relative_path_components if entry_file_type.is_file() => {
350 println!("packing \"{}\"", relative_path.display());
352
353 let input_file = File::open(entry_path).with_context(|| {
354 format!(
355 "failed to open input file from \"{}\"",
356 entry_path.display()
357 )
358 })?;
359 let metadata = input_file.metadata()?;
360 let size = u32::try_from(metadata.len())?;
361
362 file_sink.write_file(relative_path_components, size, input_file)?;
363 }
364 _ => {}
365 }
366
367 Ok(())
368}
369
370fn compile_vx(
371 entry_path: &Path,
372 entry_file_type: std::fs::FileType,
373 relative_path: &Path,
374 relative_path_components: Vec<&str>,
375 file_sink: &mut FileSink,
376) -> anyhow::Result<()> {
377 match relative_path_components.as_slice() {
378 ["Data", "Scripts.rvdata"] if entry_file_type.is_dir() => {
379 println!("packing \"{}\"", relative_path.display());
380
381 let scripts_data = generate_scripts_data(entry_path)?;
382 let size = u32::try_from(scripts_data.len())?;
383
384 file_sink.write_file(&relative_path_components, size, &*scripts_data)?;
385 }
386 ["Data", "Scripts.rvdata", ..] => {
387 }
389 ["Data", "MapInfos.rvdata"] if entry_file_type.is_dir() => {
390 println!("packing \"{}\"", relative_path.display());
391
392 let data = generate_map_infos_data(entry_path)?;
393 let size = u32::try_from(data.len())?;
394
395 file_sink.write_file(&relative_path_components, size, &*data)?;
396 }
397 ["Data", "MapInfos.rvdata", ..] => {
398 }
400 ["Data", "System.json"] if entry_file_type.is_file() => {
401 println!("packing \"{}\"", relative_path.display());
402
403 let data = generate_ruby_data::<rpgmvx_types::System>(entry_path)?;
404 let size = u32::try_from(data.len())?;
405
406 let mut relative_path_components = relative_path_components.clone();
407 *relative_path_components.last_mut().unwrap() = "System.rvdata";
408
409 file_sink.write_file(&relative_path_components, size, &*data)?;
410 }
411 ["Data", file] if crate::util::is_map_file_name(file, "json") => {
412 println!("packing \"{}\"", relative_path.display());
413
414 let map_data = generate_ruby_data::<rpgmvx_types::Map>(entry_path)?;
415 let size = u32::try_from(map_data.len())?;
416
417 let renamed_file = set_extension_str(file, "rvdata");
418 let mut relative_path_components = relative_path_components.clone();
419 *relative_path_components.last_mut().unwrap() = renamed_file.as_str();
420
421 file_sink.write_file(&relative_path_components, size, &*map_data)?;
422 }
423 relative_path_components if entry_file_type.is_file() => {
424 println!("packing \"{}\"", relative_path.display());
426
427 let input_file = File::open(entry_path).with_context(|| {
428 format!(
429 "failed to open input file from \"{}\"",
430 entry_path.display()
431 )
432 })?;
433 let metadata = input_file.metadata()?;
434 let size = u32::try_from(metadata.len())?;
435
436 file_sink.write_file(relative_path_components, size, input_file)?;
437 }
438 _ => {}
439 }
440
441 Ok(())
442}
443
444fn set_extension_str(input: &str, extension: &str) -> String {
445 let stem = input
446 .rsplit_once('.')
447 .map(|(stem, _extension)| stem)
448 .unwrap_or(input);
449
450 format!("{stem}.{extension}")
451}
452
453fn generate_scripts_data(path: &Path) -> anyhow::Result<Vec<u8>> {
454 let mut scripts_map = BTreeMap::new();
455
456 for dir_entry in path.read_dir()? {
457 let dir_entry = dir_entry?;
458 let dir_entry_file_type = dir_entry.file_type()?;
459
460 ensure!(dir_entry_file_type.is_file());
461
462 let dir_entry_file_name = dir_entry.file_name();
463 let dir_entry_file_name = dir_entry_file_name
464 .to_str()
465 .context("non-unicode script name")?;
466 let dir_entry_file_stem = dir_entry_file_name
467 .strip_suffix(".rb")
468 .context("script is not an \"rb\" file")?;
469
470 let (script_index, escaped_script_name) = dir_entry_file_stem
471 .split_once('-')
472 .context("invalid script name format")?;
473 let script_index: usize = script_index.parse()?;
474 let unescaped_file_name = crate::util::percent_unescape_file_name(escaped_script_name)?;
475
476 println!(" packing script \"{escaped_script_name}\"");
477
478 let dir_entry_path = dir_entry.path();
479 let script_data = std::fs::read_to_string(dir_entry_path)?;
480
481 let old_entry = scripts_map.insert(
482 script_index,
483 Script {
484 data: script_data,
485 id: i32::try_from(script_index)? + 1,
486 name: unescaped_file_name,
487 },
488 );
489 if old_entry.is_some() {
490 bail!("duplicate scripts for index {script_index}");
491 }
492 }
493
494 let script_list = ScriptList {
496 scripts: scripts_map.into_values().collect(),
497 };
498
499 let mut arena = ruby_marshal::ValueArena::new();
500 let handle = script_list.into_value(&mut arena)?;
501 arena.replace_root(handle);
502
503 let mut data = Vec::new();
504 ruby_marshal::dump(&mut data, &arena)?;
505
506 Ok(data)
507}
508
509fn generate_arraylike_rx_data<T>(path: &Path) -> anyhow::Result<Vec<u8>>
510where
511 T: for<'a> ArrayLikeElement<'a>,
512{
513 fn load_json_str(
514 dir_entry: std::io::Result<std::fs::DirEntry>,
515 type_display_name: &str,
516 ) -> anyhow::Result<(usize, String)> {
517 let dir_entry = dir_entry?;
518 let dir_entry_file_type = dir_entry.file_type()?;
519
520 ensure!(dir_entry_file_type.is_file());
521
522 let dir_entry_file_name = dir_entry.file_name();
523 let dir_entry_file_name = dir_entry_file_name.to_str().context("non-unicode name")?;
524 let dir_entry_file_stem = dir_entry_file_name
525 .strip_suffix(".json")
526 .context("not a \"json\" file")?;
527
528 let (index, name) = dir_entry_file_stem
529 .split_once('-')
530 .context("invalid name format")?;
531 let name = crate::util::percent_unescape_file_name(name)?;
532 let index: usize = index.parse()?;
533
534 println!(" packing {type_display_name} \"{name}\"");
535
536 let dir_entry_path = dir_entry.path();
537 let json = std::fs::read_to_string(dir_entry_path)?;
538
539 Ok((index, json))
540 }
541
542 let type_display_name = T::type_display_name();
543 let mut map: BTreeMap<usize, T> = BTreeMap::new();
544
545 for dir_entry in path.read_dir()? {
546 let (index, json) = load_json_str(dir_entry, type_display_name)?;
547 let value: T = serde_json::from_str(&json)?;
548
549 let old_entry = map.insert(index, value);
550 if old_entry.is_some() {
551 bail!("duplicate {type_display_name} for index {index}");
552 }
553 }
554
555 let mut data = Vec::with_capacity(map.len() + 1);
557 data.push(None);
558 for value in map.into_values() {
559 data.push(Some(value));
560 }
561
562 let mut arena = ruby_marshal::ValueArena::new();
563 let handle = data.into_value(&mut arena)?;
564 arena.replace_root(handle);
565
566 let mut data = Vec::new();
567 ruby_marshal::dump(&mut data, &arena)?;
568
569 Ok(data)
570}
571
572fn generate_map_infos_data(path: &Path) -> anyhow::Result<Vec<u8>> {
573 let mut map: BTreeMap<i32, rpgm_common_types::MapInfo> = BTreeMap::new();
574
575 for dir_entry in path.read_dir()? {
576 let dir_entry = dir_entry?;
577 let dir_entry_file_type = dir_entry.file_type()?;
578
579 ensure!(dir_entry_file_type.is_file());
580
581 let dir_entry_file_name = dir_entry.file_name();
582 let dir_entry_file_name = dir_entry_file_name.to_str().context("non-unicode name")?;
583 let dir_entry_file_stem = dir_entry_file_name
584 .strip_suffix(".json")
585 .context("not a \"json\" file")?;
586
587 let (index, name) = dir_entry_file_stem
588 .split_once('-')
589 .context("invalid name format")?;
590 let index: i32 = index.parse()?;
591
592 println!(" packing map info \"{name}\"");
593
594 let dir_entry_path = dir_entry.path();
595 let json = std::fs::read_to_string(dir_entry_path)?;
596
597 let value: rpgm_common_types::MapInfo = serde_json::from_str(&json)?;
598
599 let old_entry = map.insert(index, value);
600 if old_entry.is_some() {
601 bail!("duplicate map info for index {index}");
602 }
603 }
604
605 let mut arena = ruby_marshal::ValueArena::new();
606 let handle = map.into_value(&mut arena)?;
607 arena.replace_root(handle);
608
609 let mut data = Vec::new();
610 ruby_marshal::dump(&mut data, &arena)?;
611
612 Ok(data)
613}
614
615fn generate_ruby_data<T>(path: &Path) -> anyhow::Result<Vec<u8>>
616where
617 T: serde::de::DeserializeOwned + ruby_marshal::IntoValue,
618{
619 let map = std::fs::read_to_string(path)?;
620 let map: T = serde_json::from_str(&map)?;
621
622 let mut arena = ruby_marshal::ValueArena::new();
623 let handle = map.into_value(&mut arena)?;
624 arena.replace_root(handle);
625
626 let mut data = Vec::new();
627 ruby_marshal::dump(&mut data, &arena)?;
628
629 Ok(data)
630}