1mod file_entry_iter;
2mod vx;
3mod vx_ace;
4mod xp;
5
6use self::file_entry_iter::FileEntry;
7use self::file_entry_iter::FileEntryIter;
8use crate::util::ArrayLikeElement;
9use crate::GameKind;
10use anyhow::bail;
11use anyhow::ensure;
12use anyhow::Context;
13use camino::Utf8Path;
14use ruby_marshal::FromValueContext;
15use std::collections::BTreeMap;
16use std::fs::File;
17use std::io::Write;
18use std::path::Path;
19use std::path::PathBuf;
20
21fn extract_ruby_data<T>(file: impl std::io::Read, path: impl AsRef<Path>) -> anyhow::Result<()>
22where
23 T: serde::Serialize + for<'a> ruby_marshal::FromValue<'a>,
24{
25 let path = path.as_ref();
26 let path = path.with_extension("json");
27
28 let arena = ruby_marshal::load(file)?;
29 let ctx = FromValueContext::new(&arena);
30 let data: T = ctx.from_value(arena.root())?;
31
32 let temp_path = nd_util::with_push_extension(&path, "temp");
35 let mut file = File::create_new(&temp_path)?;
36 serde_json::to_writer_pretty(&mut file, &data)?;
37 file.flush()?;
38 file.sync_all()?;
39 std::fs::rename(temp_path, path)?;
40
41 Ok(())
42}
43
44fn extract_arraylike<T>(file: impl std::io::Read, dir_path: impl AsRef<Path>) -> anyhow::Result<()>
45where
46 T: for<'a> ArrayLikeElement<'a>,
47{
48 let dir_path = dir_path.as_ref();
49 let type_display_name = T::type_display_name();
50
51 std::fs::create_dir_all(dir_path)?;
52
53 let arena = ruby_marshal::load(file)?;
54 let ctx = FromValueContext::new(&arena);
55 let array: Vec<Option<T>> = ctx.from_value(arena.root())?;
56
57 for (index, value) in array.iter().enumerate() {
58 if index == 0 {
59 ensure!(value.is_none(), "{type_display_name} 0 should be nil");
60 continue;
61 }
62
63 let value = value
64 .as_ref()
65 .with_context(|| format!("{type_display_name} is nil"))?;
66
67 println!(" extracting {} \"{}\"", type_display_name, value.name());
68
69 let name = value.name();
70 let file_name = format!("{index:03}-{name}.json");
71 let file_name = crate::util::percent_escape_file_name(file_name.as_str());
72 let out_path = dir_path.join(file_name);
73 let temp_path = nd_util::with_push_extension(&out_path, "temp");
74
75 let mut output_file = File::create_new(&temp_path)?;
78 serde_json::to_writer_pretty(&mut output_file, value)?;
79 output_file.flush()?;
80 output_file.sync_all()?;
81 drop(output_file);
82
83 std::fs::rename(temp_path, out_path)?;
84 }
85
86 Ok(())
87}
88
89fn extract_scripts<P>(file: impl std::io::Read, dir_path: P) -> anyhow::Result<()>
90where
91 P: AsRef<Path>,
92{
93 let dir_path = dir_path.as_ref();
94 let temp_dir_path = nd_util::with_push_extension(dir_path, "temp");
95
96 std::fs::create_dir_all(&temp_dir_path)?;
99
100 let arena = ruby_marshal::load(file).context("failed to load ruby data")?;
101 let ctx = FromValueContext::new(&arena);
102 let script_list: rpgm_common_types::ScriptList = ctx.from_value(arena.root())?;
103
104 for (script_index, script) in script_list.scripts.iter().enumerate() {
105 println!(" extracting script \"{}\"", script.name);
106
107 let escaped_script_name = crate::util::percent_escape_file_name(&script.name);
108
109 let out_path = temp_dir_path.join(format!("{script_index:03}-{escaped_script_name}.rb"));
110 let temp_path = nd_util::with_push_extension(&out_path, "temp");
111
112 std::fs::write(&temp_path, &script.data)?;
115 std::fs::rename(temp_path, out_path)?;
116 }
117
118 std::fs::rename(temp_dir_path, dir_path)?;
119
120 Ok(())
121}
122
123fn extract_map_infos<P>(file: impl std::io::Read, dir_path: P) -> anyhow::Result<()>
124where
125 P: AsRef<Path>,
126{
127 let dir_path = dir_path.as_ref();
128
129 std::fs::create_dir_all(dir_path)?;
130
131 let arena = ruby_marshal::load(file)?;
132 let ctx = FromValueContext::new(&arena);
133 let map: BTreeMap<i32, rpgm_common_types::MapInfo> = ctx.from_value(arena.root())?;
134
135 for (index, value) in map.iter() {
136 let name = value.name.as_str();
137
138 println!(" extracting map info \"{name}\"");
139
140 let out_path = dir_path.join(format!("{index:03}-{name}.json"));
141 let temp_path = nd_util::with_push_extension(&out_path, "temp");
142
143 let mut output_file = File::create_new(&temp_path)?;
146 serde_json::to_writer_pretty(&mut output_file, value)?;
147 output_file.flush()?;
148 output_file.sync_all()?;
149 drop(output_file);
150
151 std::fs::rename(temp_path, out_path)?;
152 }
153
154 Ok(())
155}
156
157#[derive(Debug, argh::FromArgs)]
158#[argh(
159 subcommand,
160 name = "extract-assets",
161 description = "extract the assets from a game into a format that is modifiable"
162)]
163pub struct Options {
164 #[argh(
165 positional,
166 description = "the path to the game folder or rgssad archive"
167 )]
168 pub input: PathBuf,
169
170 #[argh(positional, description = "the folder to extract-assets to")]
171 pub output: PathBuf,
172
173 #[argh(
174 switch,
175 long = "overwrite",
176 description = "whether overwrite the output directory"
177 )]
178 pub overwrite: bool,
179
180 #[argh(
181 switch,
182 long = "skip-extract-scripts",
183 description = "whether scripts should not be extracted"
184 )]
185 pub skip_extract_scripts: bool,
186
187 #[argh(
188 switch,
189 long = "skip-extract-common-events",
190 description = "whether common events should not be extracted"
191 )]
192 pub skip_extract_common_events: bool,
193
194 #[argh(
195 switch,
196 long = "skip-extract-system",
197 description = "whether system data should not be extracted"
198 )]
199 pub skip_extract_system: bool,
200
201 #[argh(
202 switch,
203 long = "skip-extract-actors",
204 description = "whether actors should not be extracted"
205 )]
206 pub skip_extract_actors: bool,
207
208 #[argh(
209 switch,
210 long = "skip-extract-weapons",
211 description = "whether weapons should not be extracted"
212 )]
213 pub skip_extract_weapons: bool,
214
215 #[argh(
216 switch,
217 long = "skip-extract-armors",
218 description = "whether armor should not be extracted"
219 )]
220 pub skip_extract_armors: bool,
221
222 #[argh(
223 switch,
224 long = "skip-extract-skills",
225 description = "whether skills should not be extracted"
226 )]
227 pub skip_extract_skills: bool,
228
229 #[argh(
230 switch,
231 long = "skip-extract-states",
232 description = "whether states should not be extracted"
233 )]
234 pub skip_extract_states: bool,
235
236 #[argh(
237 switch,
238 long = "skip-extract-items",
239 description = "whether items should not be extracted"
240 )]
241 pub skip_extract_items: bool,
242
243 #[argh(
244 switch,
245 long = "skip-extract-enemies",
246 description = "whether enemies should not be extracted"
247 )]
248 pub skip_extract_enemies: bool,
249
250 #[argh(
251 switch,
252 long = "skip-extract-classes",
253 description = "whether classes should not be extracted"
254 )]
255 pub skip_extract_classes: bool,
256
257 #[argh(
258 switch,
259 long = "skip-extract-troops",
260 description = "whether troops should not be extracted"
261 )]
262 pub skip_extract_troops: bool,
263
264 #[argh(
265 switch,
266 long = "skip-extract-tilesets",
267 description = "whether tilesets should not be extracted"
268 )]
269 pub skip_extract_tilesets: bool,
270
271 #[argh(
272 switch,
273 long = "skip-extract-map-infos",
274 description = "whether map infos should not be extracted"
275 )]
276 pub skip_extract_map_infos: bool,
277
278 #[argh(
279 switch,
280 long = "skip-extract-animations",
281 description = "whether animations should not be extracted"
282 )]
283 pub skip_extract_animations: bool,
284
285 #[argh(
286 switch,
287 long = "skip-extract-maps",
288 description = "whether maps should not be extracted"
289 )]
290 pub skip_extract_maps: bool,
291}
292
293pub fn exec(mut options: Options) -> anyhow::Result<()> {
294 options.input = options
295 .input
296 .canonicalize()
297 .context("failed to canonicalize input path")?;
298
299 if options.output.try_exists()? {
300 if options.overwrite {
301 std::fs::remove_dir_all(&options.output)?;
302 } else {
303 bail!("output path exists");
304 }
305 }
306
307 std::fs::create_dir_all(&options.output)?;
310
311 options.output = options
312 .output
313 .canonicalize()
314 .context("failed to canonicalize output path")?;
315
316 let mut file_entry_iter = FileEntryIter::new(&options.input)?;
317 let game_kind = file_entry_iter.game_kind();
318
319 while let Some(mut entry) = file_entry_iter.next_file_entry()? {
320 let raw_relative_path = entry.relative_path().to_path_buf();
321 let relative_path_components = parse_relative_path(&raw_relative_path)?;
322 let relative_path_display = relative_path_components.join("/");
323 let output_path = {
324 let mut output_path = options.output.clone();
325 output_path.extend(relative_path_components.clone());
326 output_path
327 };
328
329 println!("extracting \"{relative_path_display}\"");
330
331 if let Some(parent) = output_path.parent() {
332 std::fs::create_dir_all(parent)
333 .with_context(|| format!("failed to create dir at \"{}\"", parent.display()))?;
334 }
335
336 match game_kind {
337 GameKind::Xp => {
338 self::xp::extract(&options, &mut entry, relative_path_components, output_path)?
339 }
340 GameKind::Vx => {
341 self::vx::extract(&options, &mut entry, relative_path_components, output_path)?
342 }
343 GameKind::VxAce => {
344 self::vx_ace::extract(&options, &mut entry, relative_path_components, output_path)?
345 }
346 }
347 }
348
349 Ok(())
350}
351
352fn parse_relative_path(path: &Utf8Path) -> anyhow::Result<Vec<&str>> {
353 let mut components = Vec::with_capacity(4);
354
355 for component in path.as_str().split(['/', '\\']) {
371 ensure!(!component.is_empty());
372 ensure!(component != "..");
373 ensure!(!component.contains(':'));
374
375 if component == "." {
376 continue;
377 }
378
379 components.push(component);
380 }
381
382 Ok(components)
383}