rpgmxp_project/commands/
init.rs

1use 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        // We will add more later.
88        #[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        // TODO: Should we allow files in non-standard places?
115        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}