1pub use larian_formats;
2
3use anyhow::{Result, anyhow, bail};
4use clap::ValueEnum;
5use larian_formats::{
6 lsf::LsfData,
7 lspk::{self, Lspk, ModuleInfo, is_override},
8};
9use std::{
10 collections::VecDeque,
11 ffi::OsStr,
12 fs::{File, read_dir},
13 path::{Path, PathBuf},
14};
15
16#[derive(Debug, ValueEnum, Clone, Copy)]
18pub enum DataFileType {
19 Lsf,
20 Lsx,
21}
22
23impl DataFileType {
24 fn from_path(path: &Path) -> Option<Self> {
25 match path.extension().map(|s| s.to_string_lossy()).as_deref() {
26 Some("lsf") => Some(Self::Lsf),
27 Some("lsx") => Some(Self::Lsx),
28 _ => None,
29 }
30 }
31
32 const fn extension(self) -> &'static str {
33 match self {
34 Self::Lsf => "lsf",
35 Self::Lsx => "lsx",
36 }
37 }
38}
39
40macro_rules! convert_inner {
41 (
42 $from: expr,
43 $to: expr,
44 $read_fn: expr,
45 $write_fn: expr $(,)?
46 ) => {
47 let mut input = File::open($from)?;
48 let data = $read_fn(&mut input)
49 .map_err(|e| anyhow!("Failed to parse {}: {e}", $from.display()))?;
50 let mut out = File::options()
51 .create(true)
52 .write(true)
53 .truncate(true)
54 .open($to)?;
55 $write_fn(&data, &mut out).map_err(|e| {
56 anyhow!("Failed to serialize data from {} into {}: {e}", $from.display(), $to.display())
57 })?;
58 eprintln!("Converted {} to {}", $from.display(), $to.display());
59 };
60}
61
62#[cfg(unix)]
68pub fn contents(path: &Path) -> Result<()> {
69 use std::os::unix::ffi::OsStrExt;
70
71 let paths = larian_formats::lspk::list_mod_pack_files(path)?;
72
73 for bytes in paths.iter() {
74 println!("{}", OsStr::from_bytes(bytes.as_ref()).display());
75 }
76
77 Ok(())
78}
79
80#[cfg(unix)]
88pub fn extract_file(from: &Path, internal: &Path, to: &Path) -> Result<()> {
89 use anyhow::Context;
90
91 let Some(bytes) = larian_formats::raw::extract_file_from_pak(internal, from)? else {
92 bail!("{} not found in {}", internal.display(), from.display());
93 };
94
95 std::fs::write(to, bytes).with_context(|| {
96 format!(
97 "could not write {} from {} to {}",
98 internal.display(),
99 from.display(),
100 to.display()
101 )
102 })
103}
104
105pub fn convert_all(path: PathBuf, from: DataFileType, to: DataFileType) -> Result<()> {
112 let mut dirs = VecDeque::from_iter([path]);
113
114 while let Some(dir) = dirs.pop_front() {
115 for entry in read_dir(dir)? {
116 let e = entry?;
117 let file_type = e.file_type()?;
118
119 if file_type.is_dir() {
120 dirs.push_back(e.path());
121 } else if file_type.is_file() &&
122 e.path().extension() == Some(OsStr::new(from.extension()))
123 {
124 let mut dest = e.path().clone();
125 dest.set_extension(to.extension());
126 convert(&e.path(), &dest)?;
127 }
128 }
129 }
130
131 Ok(())
132}
133
134pub fn convert(from: &Path, to: &Path) -> Result<()> {
140 let Some(from_type) = DataFileType::from_path(from) else {
141 bail!("`{}` does not have either `.lsf` or `.lsx` extension", from.display());
142 };
143
144 let Some(to_type) = DataFileType::from_path(to) else {
145 bail!("`{}` does not have either `.lsf` or `.lsx` extension", to.display());
146 };
147
148 match (from_type, to_type) {
149 (DataFileType::Lsf, DataFileType::Lsx) => {
150 convert_inner!(from, to, LsfData::read_lsf, LsfData::write_lsx);
151 }
152 (DataFileType::Lsx, DataFileType::Lsf) => {
153 convert_inner!(from, to, LsfData::read_lsx, LsfData::write_lsf);
154 }
155 (..) => {
156 std::fs::copy(from, to)?;
157 }
158 }
159
160 Ok(())
161}
162
163pub fn pack(mod_files_root: PathBuf, destination: Option<PathBuf>) -> Result<()> {
169 lspk::write(mod_files_root, destination)?;
170 Ok(())
171}
172
173pub fn unpack(mod_file_path: &Path, destination: Option<PathBuf>) -> Result<()> {
179 let data = Lspk::from_file(mod_file_path)?;
180
181 let prefix_dir = match destination {
182 Some(unpack_dir) => {
183 std::fs::create_dir_all(&unpack_dir)?;
184 unpack_dir
185 }
186 None => "./".into(),
187 };
188
189 for file in data.files {
190 let path = prefix_dir.join(file.path);
191
192 if let Some(parent) = path.parent() {
193 std::fs::create_dir_all(parent)?;
194 }
195
196 std::fs::write(path, file.contents)?;
197 }
198
199 Ok(())
200}
201
202pub fn parse(paths: impl IntoIterator<Item = PathBuf>, verbose: bool) -> Result<()> {
208 for path in paths {
209 let displayed_path = path.display().to_string();
210 let data = Lspk::from_file(&path)?;
211
212 print!("{displayed_path}");
213
214 if is_override(path) {
215 print!(" (override)");
216 }
217
218 println!();
219
220 for _ in displayed_path.chars() {
221 print!("-");
222 }
223
224 println!();
225
226 print_lspk(&data, verbose)?;
227
228 println!();
229 }
230
231 Ok(())
232}
233
234fn print_lspk(data: &Lspk, verbose: bool) -> Result<()> {
235 let meta_lsx = data.deserialize_meta_lsx()?;
236
237 let ModuleInfo {
238 author,
239 description,
240 folder,
241 md5,
242 name,
243 num_players,
244 module_type,
245 uuid,
246 version,
247 ..
248 } = meta_lsx.module_info;
249
250 println!("Name : {name}");
251 println!("Folder : {folder}");
252 println!("Version64 : {version}");
253 println!("UUID : {uuid}");
254
255 if !verbose {
256 return Ok(());
257 }
258
259 println!("Author : {author}");
260 println!("Description : {description}");
261 println!("MD5 : {md5}");
262 println!("NumPlayers : {num_players}");
263 println!("Type : {module_type}");
264
265 Ok(())
266}