rpgmxp_project/commands/
init.rs1use anyhow::ensure;
2use anyhow::Context;
3use std::fmt::Write;
4use std::path::Path;
5use std::path::PathBuf;
6
7#[derive(Debug, argh::FromArgs)]
8#[argh(name = "init", subcommand, description = "init a new project")]
9pub struct Options {
10 #[argh(
11 option,
12 description = "the path to the game or archive to init a project from"
13 )]
14 pub from: PathBuf,
15
16 #[argh(option, description = "the output path", default = "PathBuf::new()")]
17 pub output: PathBuf,
18}
19
20pub fn exec(options: Options) -> anyhow::Result<()> {
21 copy_data(&options.from, &options.output)?;
22 copy_graphics(&options.from, &options.output)?;
23 Ok(())
24}
25
26fn copy_data(base_in_path: &Path, base_out_path: &Path) -> anyhow::Result<()> {
27 let data_path = base_in_path.join("Data");
28 ensure!(
29 data_path.exists(),
30 "missing folder at \"{}\"",
31 data_path.display()
32 );
33 let out_dir = base_out_path.join("Data");
34 for entry in std::fs::read_dir(data_path)? {
35 let entry = entry?;
36
37 let file_type = entry.file_type()?;
38 ensure!(file_type.is_file());
39
40 let in_path = entry.path();
41 ensure!(in_path.extension() == Some("rxdata".as_ref()));
42
43 let file_stem = in_path
44 .file_stem()
45 .context("missing file stem")?
46 .to_str()
47 .context("file stem is not valid unicode")?;
48
49 let map_number = file_stem.strip_prefix("Map").and_then(|file_stem| {
50 if file_stem.len() != 3 {
51 return None;
52 }
53
54 if !file_stem.chars().all(|c| c.is_ascii_digit()) {
55 return None;
56 }
57
58 Some(file_stem)
59 });
60
61 if let Some(map_number) = map_number {
62 let map_data = std::fs::read(&in_path)?;
63 let value_arena = ruby_marshal::load(&*map_data)?;
64 let ctx = ruby_marshal::FromValueContext::new(&value_arena);
65
66 let maybe_map: Result<rpgmxp_types::Map, _> = ctx.from_value(value_arena.root());
67
68 if let Err(ruby_marshal::FromValueError::UnexpectedValueKind { kind, trace }) =
69 maybe_map.as_ref()
70 {
71 dbg!(kind);
72 for handle in trace.iter().copied() {
73 let value = value_arena.get(handle).unwrap();
74 dbg!(DebugValue::new(&value_arena, value, 10));
75 }
76 }
77
78 let map = maybe_map
79 .with_context(|| format!("failed to extract data from Map{map_number:03}"))?;
80
81 let out_path = out_dir.join(format!("Map{map_number}.json"));
82 std::fs::write(&out_path, &serde_json::to_string_pretty(&map)?)?;
83
84 continue;
85 }
86
87 #[allow(clippy::single_match)]
89 match file_stem {
90 "Scripts" => {
91 let out_dir = out_dir.join("Scripts");
92 std::fs::create_dir_all(&out_dir)?;
93 extract_scripts(&in_path, &out_dir)?;
94 }
95 _ => {}
96 }
97 }
98
99 Ok(())
100}
101
102fn copy_graphics(base_in_path: &Path, base_out_path: &Path) -> anyhow::Result<()> {
103 let graphics_path = base_in_path.join("Graphics");
104
105 ensure!(
106 graphics_path.exists(),
107 "missing folder at \"{}\"",
108 graphics_path.display()
109 );
110 let out_dir = base_out_path.join("Graphics");
111 for entry in std::fs::read_dir(graphics_path)? {
112 let entry = entry?;
113
114 let file_type = entry.file_type()?;
116 ensure!(file_type.is_dir());
117
118 let dir_name = entry.file_name();
119 let out_dir = out_dir.join(dir_name);
120
121 std::fs::create_dir_all(&out_dir)?;
122
123 for entry in std::fs::read_dir(entry.path())? {
124 let entry = entry?;
125
126 let file_type = entry.file_type()?;
127 ensure!(file_type.is_file());
128
129 let in_path = entry.path();
130 let out_path = out_dir.join(entry.file_name());
131 std::fs::copy(&in_path, &out_path).with_context(|| {
132 format!(
133 "failed to copy \"{}\" to \"{}\"",
134 in_path.display(),
135 out_path.display()
136 )
137 })?;
138 }
139 }
140
141 Ok(())
142}
143
144fn extract_scripts(in_path: &Path, out_dir: &Path) -> anyhow::Result<()> {
145 let scripts_data = std::fs::read(in_path)?;
146 let value_arena = ruby_marshal::load(&*scripts_data)?;
147 let ctx = ruby_marshal::FromValueContext::new(&value_arena);
148
149 let script_list: rpgmxp_types::ScriptList = ctx.from_value(value_arena.root())?;
150
151 for (script_index, script) in script_list.scripts.iter().enumerate() {
152 let escaped_script_name = escape_file_name(&script.name);
153
154 let out_path = out_dir.join(format!("{script_index}-{escaped_script_name}.rb"));
155 std::fs::write(&out_path, &script.data)?;
156 }
157
158 Ok(())
159}
160
161fn escape_file_name(file_name: &str) -> String {
162 let mut escaped = String::with_capacity(file_name.len());
163 for c in file_name.chars() {
164 match c {
165 '%' | ':' => {
166 let c = u32::from(c);
167 write!(&mut escaped, "%{c:02x}").unwrap();
168 }
169 _ => {
170 escaped.push(c);
171 }
172 }
173 }
174 escaped
175}
176
177struct DebugValue<'a> {
178 arena: &'a ruby_marshal::ValueArena,
179 value: &'a ruby_marshal::Value,
180 limit: usize,
181}
182
183impl<'a> DebugValue<'a> {
184 fn new(
185 arena: &'a ruby_marshal::ValueArena,
186 value: &'a ruby_marshal::Value,
187 limit: usize,
188 ) -> Self {
189 Self {
190 arena,
191 value,
192 limit,
193 }
194 }
195}
196
197impl std::fmt::Debug for DebugValue<'_> {
198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 if self.limit == 0 {
200 return self.value.fmt(f);
201 }
202
203 match &self.value {
204 ruby_marshal::Value::Bool(value) => value.value().fmt(f),
205 ruby_marshal::Value::Fixnum(value) => value.value().fmt(f),
206 ruby_marshal::Value::String(value) => {
207 let value = value.value();
208 match std::str::from_utf8(value) {
209 Ok(value) => value.fmt(f),
210 Err(_error) => value.fmt(f),
211 }
212 }
213 ruby_marshal::Value::Array(value) => {
214 let mut f = f.debug_list();
215 for handle in value.value().iter().copied() {
216 match self.arena.get(handle) {
217 Some(value) => {
218 f.entry(&DebugValue::new(self.arena, value, self.limit - 1));
219 }
220 None => {
221 f.entry(&handle);
222 }
223 }
224 }
225 f.finish()
226 }
227 ruby_marshal::Value::Object(value) => {
228 let name = value.name();
229 let name = match self
230 .arena
231 .get_symbol(name)
232 .and_then(|value| std::str::from_utf8(value.value()).ok())
233 {
234 Some(name) => name,
235 None => {
236 return value.fmt(f);
237 }
238 };
239
240 let instance_variables = value.instance_variables();
241
242 let mut f = f.debug_struct(name);
243
244 for (key, value) in instance_variables.iter().copied() {
245 let key = self.arena.get_symbol(key).unwrap().value();
246 let key = std::str::from_utf8(key).unwrap();
247
248 let value = self.arena.get(value).unwrap();
249
250 f.field(key, &DebugValue::new(self.arena, value, self.limit - 1));
251 }
252
253 f.finish()
254 }
255 _ => self.value.fmt(f),
256 }
257 }
258}