1use crate::{CPXVersion, CampaignHeader, ScenarioMeta};
2use byteorder::{ReadBytesExt, LE};
3use chardet::detect as detect_encoding;
4use encoding_rs::Encoding;
5use genie_scx::{self as scx, DLCPackage, Scenario};
6use std::convert::TryFrom;
7use std::io::{self, Cursor, Read, Seek, SeekFrom};
8
9#[derive(Debug, thiserror::Error)]
11pub enum ReadCampaignError {
12 #[error("invalid string")]
14 DecodeStringError,
15 #[error("{}", .0)]
17 IoError(#[from] io::Error),
18 #[error("campaign or scenario must have a name")]
20 MissingNameError,
21 #[error("scenario not fonud in campaign")]
23 NotFoundError,
24 #[error("{}", .0)]
26 ParseSCXError(#[from] scx::Error),
27}
28
29type Result<T> = std::result::Result<T, ReadCampaignError>;
30
31fn decode_str(bytes: &[u8]) -> Result<String> {
33 if bytes.is_empty() {
34 return Ok("".to_string());
35 }
36
37 let (encoding_name, _confidence, _language) = detect_encoding(&bytes);
38 Encoding::for_label(encoding_name.as_bytes())
39 .ok_or(ReadCampaignError::DecodeStringError)
40 .and_then(|encoding| {
41 let (decoded, _enc, failed) = encoding.decode(&bytes);
42 if failed {
43 return Err(ReadCampaignError::DecodeStringError);
44 }
45 Ok(decoded.to_string())
46 })
47}
48
49pub fn read_fixed_str<R: Read>(input: &mut R, len: usize) -> Result<Option<String>> {
50 let mut bytes = vec![0; len];
51 input.read_exact(&mut bytes[0..len])?;
52
53 if let Some(end) = bytes.iter().position(|&byte| byte == 0) {
54 bytes.truncate(end);
55 }
56 if bytes.is_empty() {
57 Ok(None)
58 } else {
59 decode_str(&bytes).map(Some)
60 }
61}
62
63fn read_hd_or_later_string<R: Read>(input: &mut R) -> Result<Option<String>> {
64 let open = input.read_u16::<LE>()?;
65 if open != 0x0A60 {
67 return Err(ReadCampaignError::DecodeStringError);
68 }
69 let len = input.read_u16::<LE>()? as usize;
70 let mut bytes = vec![0; len];
71 input.read_exact(&mut bytes[0..len])?;
72 decode_str(&bytes).map(Some)
73}
74
75fn read_campaign_header<R: Read>(input: &mut R) -> Result<CampaignHeader> {
76 let mut version = [0; 4];
77 input.read_exact(&mut version)?;
78 let (name, num_scenarios) = if version == *b"2.00" {
79 let num_dependencies = input.read_u32::<LE>()?;
80 let mut dependencies = vec![DLCPackage::AgeOfKings; num_dependencies as usize];
81 for dependency in dependencies.iter_mut() {
82 *dependency =
83 DLCPackage::try_from(input.read_i32::<LE>()?).map_err(scx::Error::from)?;
84 }
85
86 let name = read_fixed_str(input, 256)?.ok_or(ReadCampaignError::MissingNameError)?;
87
88 let num_scenarios = input.read_u32::<LE>()? as usize;
89 (name, num_scenarios)
90 } else if version == *b"1.10" {
91 let num_scenarios = input.read_u32::<LE>()? as usize;
92 (
93 read_hd_or_later_string(input)?.ok_or(ReadCampaignError::MissingNameError)?,
94 num_scenarios,
95 )
96 } else {
97 (
98 read_fixed_str(input, 256)?.ok_or(ReadCampaignError::MissingNameError)?,
99 input.read_u32::<LE>()? as usize,
100 )
101 };
102
103 Ok(CampaignHeader {
104 version,
105 name,
106 num_scenarios,
107 })
108}
109
110fn read_scenario_meta_de2<R: Read>(input: &mut R) -> Result<ScenarioMeta> {
111 let size = input.read_u32::<LE>()? as usize;
112 let offset = input.read_u32::<LE>()? as usize;
113 let name = read_hd_or_later_string(input)?.ok_or(ReadCampaignError::MissingNameError)?;
114 let filename = read_hd_or_later_string(input)?.ok_or(ReadCampaignError::MissingNameError)?;
115
116 Ok(ScenarioMeta {
117 size,
118 offset,
119 name,
120 filename,
121 })
122}
123
124fn read_scenario_meta_de<R: Read>(input: &mut R) -> Result<ScenarioMeta> {
125 let size = input.read_u64::<LE>()? as usize;
126 let offset = input.read_u64::<LE>()? as usize;
127 let name = read_hd_or_later_string(input)?.ok_or(ReadCampaignError::MissingNameError)?;
128 let filename = read_hd_or_later_string(input)?.ok_or(ReadCampaignError::MissingNameError)?;
129
130 Ok(ScenarioMeta {
131 size,
132 offset,
133 name,
134 filename,
135 })
136}
137
138fn read_scenario_meta<R: Read>(input: &mut R) -> Result<ScenarioMeta> {
139 let size = input.read_i32::<LE>()? as usize;
140 let offset = input.read_i32::<LE>()? as usize;
141 let name = read_fixed_str(input, 255)?.ok_or(ReadCampaignError::MissingNameError)?;
142 let filename = read_fixed_str(input, 255)?.ok_or(ReadCampaignError::MissingNameError)?;
143 let mut padding = [0; 2];
144 input.read_exact(&mut padding)?;
145
146 Ok(ScenarioMeta {
147 size,
148 offset,
149 name,
150 filename,
151 })
152}
153
154#[derive(Debug, Clone)]
156pub struct Campaign<R>
157where
158 R: Read + Seek,
159{
160 reader: R,
161 header: CampaignHeader,
162 entries: Vec<ScenarioMeta>,
163}
164
165impl<R> Campaign<R>
166where
167 R: Read + Seek,
168{
169 pub fn from(mut input: R) -> Result<Self> {
174 let header = read_campaign_header(&mut input)?;
175 let mut entries = vec![];
176 let read_entry = if header.version == *b"2.00" {
177 read_scenario_meta_de2
178 } else if header.version == *b"1.10" {
179 read_scenario_meta_de
180 } else {
181 read_scenario_meta
182 };
183 for _ in 0..header.num_scenarios {
184 entries.push(read_entry(&mut input)?);
185 }
186
187 Ok(Self {
188 reader: input,
189 header,
190 entries,
191 })
192 }
193
194 pub fn into_inner(self) -> R {
196 self.reader
197 }
198
199 pub fn version(&self) -> CPXVersion {
201 self.header.version
202 }
203
204 pub fn name(&self) -> &str {
206 &self.header.name
207 }
208
209 pub fn entries(&self) -> impl Iterator<Item = &ScenarioMeta> {
211 self.entries.iter()
212 }
213
214 pub fn len(&self) -> usize {
216 self.entries.len()
217 }
218
219 pub fn is_empty(&self) -> bool {
221 self.entries.is_empty()
222 }
223
224 pub fn get_name(&self, id: usize) -> Option<&str> {
226 self.entries.get(id).map(|entry| entry.name.as_ref())
227 }
228
229 pub fn get_filename(&self, id: usize) -> Option<&str> {
231 self.entries.get(id).map(|entry| entry.filename.as_ref())
232 }
233
234 fn get_id(&self, filename: &str) -> Option<usize> {
236 self.entries
237 .iter()
238 .position(|entry| entry.filename == filename)
239 }
240
241 pub fn by_name(&mut self, filename: &str) -> Result<Scenario> {
243 self.by_name_raw(filename)
244 .map(Cursor::new)
245 .and_then(|buf| Scenario::read_from(buf).map_err(ReadCampaignError::ParseSCXError))
246 }
247
248 pub fn by_index(&mut self, index: usize) -> Result<Scenario> {
250 self.by_index_raw(index)
251 .map(Cursor::new)
252 .and_then(|buf| Scenario::read_from(buf).map_err(ReadCampaignError::ParseSCXError))
253 }
254
255 pub fn by_name_raw(&mut self, filename: &str) -> Result<Vec<u8>> {
257 self.get_id(filename)
258 .ok_or(ReadCampaignError::NotFoundError)
259 .and_then(|index| self.by_index_raw(index))
260 }
261
262 pub fn by_index_raw(&mut self, index: usize) -> Result<Vec<u8>> {
264 let entry = match self.entries.get(index) {
265 Some(entry) => entry,
266 None => return Err(ReadCampaignError::NotFoundError),
267 };
268
269 let mut result = vec![];
270
271 self.reader.seek(SeekFrom::Start(entry.offset as u64))?;
272 self.reader
273 .by_ref()
274 .take(entry.size as u64)
275 .read_to_end(&mut result)?;
276
277 Ok(result)
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use crate::{AOE1_DE, AOE2_DE, AOE_AOK};
285 use anyhow::Context;
286 use std::fs::File;
287
288 #[test]
291 fn detect_encoding() -> anyhow::Result<()> {
292 let f = File::open("./test/campaigns/DER FALL VON SACSAHUAMAN - TEIL I.cpx")?;
293 let cpx = Campaign::from(f)?;
294 assert_eq!(cpx.version(), AOE_AOK);
295 assert_eq!(cpx.name(), "DER FALL VON SACSAHUAMÁN - TEIL I");
296 assert_eq!(cpx.len(), 1);
297
298 let names: Vec<_> = cpx.entries().map(|e| &e.name).collect();
299 assert_eq!(names, vec!["Der Weg nach Sacsahuamán"]);
300 let filenames: Vec<_> = cpx.entries().map(|e| &e.filename).collect();
301 assert_eq!(filenames, vec!["Der Weg nach Sacsahuamán.scx"]);
302 Ok(())
303 }
304
305 #[test]
307 fn aoe1_trial_cpn() -> anyhow::Result<()> {
308 let f = File::open("test/campaigns/Armies at War A Combat Showcase.cpn")?;
309 let mut c = Campaign::from(f).context("could not read meta")?;
310
311 assert_eq!(c.version(), AOE_AOK);
312 assert_eq!(c.name(), "Armies at War, A Combat Showcase");
313 assert_eq!(c.len(), 1);
314 let names: Vec<_> = c.entries().map(|e| &e.name).collect();
315 assert_eq!(names, vec!["Bronze Age Art of War"]);
316 let filenames: Vec<_> = c.entries().map(|e| &e.filename).collect();
317 assert_eq!(filenames, vec!["Bronze Age Art of War.scn"]);
318
319 c.by_index_raw(0).context("could not read raw file")?;
320 c.by_name_raw("Bronze Age Art of War.scn")
321 .context("could not read raw file")?;
322 Ok(())
323 }
324
325 #[test]
326 fn aoe1_beta_cpn() -> anyhow::Result<()> {
327 let f = File::open("test/campaigns/Rise of Egypt Learning Campaign.cpn")?;
328 let c = Campaign::from(f).context("could not read meta")?;
329
330 assert_eq!(c.version(), AOE_AOK);
331 assert_eq!(c.name(), "Rise of Egypt Learning Campaign");
332 assert_eq!(c.len(), 12);
333 let filenames: Vec<_> = c.entries().map(|e| &e.filename).collect();
334 assert_eq!(
335 filenames,
336 vec![
337 "HUNTING.scn",
338 "FORAGING.scn",
339 "Discoveries.scn",
340 "Dawn of a New Age.scn",
341 "SKIRMISH.scn",
342 "Lands Unknown.scn",
343 "FARMING.scn",
344 "TRADE.scn",
345 "CRUSADE.scn",
346 "Establish a Second Colony.scn",
347 "Naval Battle.scn",
348 "Siege Battle.scn",
349 ]
350 );
351 Ok(())
352 }
353
354 #[test]
355 fn aoe_de() -> anyhow::Result<()> {
356 let f = File::open("test/campaigns/10 The First Punic War.aoecpn")?;
357 let c = Campaign::from(f)?;
358
359 assert_eq!(c.version(), AOE1_DE);
360 assert_eq!(c.name(), "10 The First Punic War");
361 assert_eq!(c.len(), 3);
362 let filenames: Vec<_> = c.entries().map(|e| &e.filename).collect();
363 assert_eq!(
364 filenames,
365 vec![
366 "Scxt1-01-The Battle of Agrigentum.aoescn",
367 "Scxt1-02-The Battle of Mylae.aoescn",
368 "Scxt1-03-The Battle of Tunis.aoescn",
369 ]
370 );
371
372 Ok(())
380 }
381
382 #[test]
383 fn aoe_de2() -> Result<()> {
384 let f = File::open("test/campaigns/acam1.aoe2campaign")?;
385 let c = Campaign::from(f)?;
386
387 assert_eq!(c.version(), AOE2_DE);
388 assert_eq!(c.name(), "acam1");
389 assert_eq!(c.len(), 5);
390 let filenames: Vec<_> = c.entries().map(|e| &e.filename).collect();
391 assert_eq!(
392 filenames,
393 vec![
394 "A1_Tariq1.aoe2scenario",
395 "A1_Tariq2.aoe2scenario",
396 "A1_Tariq3.aoe2scenario",
397 "A1_Tariq4.aoe2scenario",
398 "A1_Tariq5.aoe2scenario",
399 ]
400 );
401
402 let f = File::open("test/campaigns/rcam3.aoe2campaign")?;
403 let c = Campaign::from(f)?;
404
405 assert_eq!(c.version(), AOE2_DE);
406 assert_eq!(c.name(), "rcam3");
407 assert_eq!(c.len(), 5);
408 let filenames: Vec<_> = c.entries().map(|e| &e.filename).collect();
409 assert_eq!(
410 filenames,
411 vec![
412 "R3_Bayinnaung_1.aoe2scenario",
413 "R3_Bayinnaung_2.aoe2scenario",
414 "R3_Bayinnaung_3.aoe2scenario",
415 "R3_Bayinnaung_4.aoe2scenario",
416 "R3_Bayinnaung_5.aoe2scenario",
417 ]
418 );
419
420 Ok(())
428 }
429}