1use std::io::{Cursor, Read};
24
25use crate::types::*;
26
27#[derive(Debug)]
29pub enum S2Error {
30 Io(std::io::Error),
31 InvalidHeader(String),
32 UnexpectedEof,
33 InvalidTrailer(String),
34 UnsupportedFormat(String),
35}
36
37impl std::fmt::Display for S2Error {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 match self {
40 S2Error::Io(e) => write!(f, "I/O error: {e}"),
41 S2Error::InvalidHeader(s) => write!(f, "invalid header: {s}"),
42 S2Error::UnexpectedEof => write!(f, "unexpected end of file"),
43 S2Error::InvalidTrailer(s) => write!(f, "invalid trailer: {s}"),
44 S2Error::UnsupportedFormat(s) => write!(f, "unsupported format: {s}"),
45 }
46 }
47}
48
49impl std::error::Error for S2Error {
50 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
51 match self {
52 S2Error::Io(e) => Some(e),
53 _ => None,
54 }
55 }
56}
57
58impl From<std::io::Error> for S2Error {
59 fn from(e: std::io::Error) -> Self {
60 S2Error::Io(e)
61 }
62}
63
64pub type S2Result<T> = Result<T, S2Error>;
66
67fn read_u8(c: &mut Cursor<&[u8]>) -> S2Result<u8> {
72 let mut buf = [0u8; 1];
73 c.read_exact(&mut buf)?;
74 Ok(buf[0])
75}
76
77fn read_u16_le(c: &mut Cursor<&[u8]>) -> S2Result<u16> {
78 let mut buf = [0u8; 2];
79 c.read_exact(&mut buf)?;
80 Ok(u16::from_le_bytes(buf))
81}
82
83fn read_u32_le(c: &mut Cursor<&[u8]>) -> S2Result<u32> {
84 let mut buf = [0u8; 4];
85 c.read_exact(&mut buf)?;
86 Ok(u32::from_le_bytes(buf))
87}
88
89fn read_f32_le(c: &mut Cursor<&[u8]>) -> S2Result<f32> {
90 let mut buf = [0u8; 4];
91 c.read_exact(&mut buf)?;
92 Ok(f32::from_le_bytes(buf))
93}
94
95fn read_exact(c: &mut Cursor<&[u8]>, n: usize) -> S2Result<Vec<u8>> {
96 let mut buf = vec![0u8; n];
97 c.read_exact(&mut buf)?;
98 Ok(buf)
99}
100
101fn read_s2_string(c: &mut Cursor<&[u8]>) -> S2Result<String> {
104 let len = read_u32_le(c)? as usize;
105 if len == 0 {
106 return Ok(String::new());
107 }
108 let bytes = read_exact(c, len)?;
109 Ok(String::from_utf8_lossy(&bytes).into_owned())
110}
111
112fn read_s2_line(c: &mut Cursor<&[u8]>) -> S2Result<String> {
114 let mut bytes = Vec::new();
115 loop {
116 let b = read_u8(c)?;
117 if b == b'\r' {
118 let next = read_u8(c)?;
119 if next == b'\n' {
120 break;
121 }
122 bytes.push(b);
123 bytes.push(next);
124 } else {
125 bytes.push(b);
126 }
127 }
128 Ok(String::from_utf8_lossy(&bytes).into_owned())
129}
130
131pub fn parse_s2(data: &[u8]) -> S2Result<S2Map> {
137 let mut c = Cursor::new(data);
138
139 let header = parse_header(&mut c)?;
141
142 let minimap = read_exact(&mut c, 20736)?;
144
145 let pw_key = read_u8(&mut c)?;
148 let encoded_pw = read_s2_line(&mut c)?;
149
150 let env_vars = parse_env_vars(&mut c)?;
152
153 let quickslots = parse_quickslots(&mut c)?;
155
156 let colormap_dim = read_u32_le(&mut c)?;
158 let colormap_size = (colormap_dim * colormap_dim * 3) as usize;
159 let colormap = read_exact(&mut c, colormap_size)?;
160
161 let terrain_size = read_u32_le(&mut c)?;
163 let height_count = ((terrain_size + 1) * (terrain_size + 1)) as usize;
164 let mut heights = Vec::with_capacity(height_count);
165 for _ in 0..height_count {
166 heights.push(read_f32_le(&mut c)?);
167 }
168
169 let grass_dim = colormap_dim + 1;
171 let grass_size = (grass_dim * grass_dim) as usize;
172 let grass = read_exact(&mut c, grass_size)?;
173
174 let (objects, units, items, infos, states, extensions, trailer) = parse_footer(&mut c)?;
176
177 let decoded_pw = decode_password(&encoded_pw, pw_key);
179
180 Ok(S2Map {
181 header,
182 minimap,
183 password: S2Password {
184 key: pw_key,
185 encoded: encoded_pw,
186 decoded: decoded_pw,
187 },
188 env_vars,
189 quickslots,
190 colormap_dim,
191 colormap,
192 terrain_size,
193 heights,
194 grass,
195 objects,
196 units,
197 items,
198 infos,
199 states,
200 extensions,
201 trailer,
202 })
203}
204
205fn decode_password(encoded: &str, key: u8) -> String {
207 let bytes = encoded.as_bytes();
208 let decoded: Vec<u8> = bytes.iter().map(|&b| b ^ key).collect();
209 String::from_utf8_lossy(&decoded).into_owned()
210}
211
212fn parse_header(c: &mut Cursor<&[u8]>) -> S2Result<S2Header> {
217 let mut lines: Vec<String> = Vec::new();
218 loop {
219 let line = read_s2_line(c)?;
220 if line == "###" {
221 break;
222 }
223 lines.push(line);
224 }
225
226 if lines.len() < 11 {
227 return Err(S2Error::InvalidHeader(format!(
228 "expected 11+ header lines before '###', got {}",
229 lines.len()
230 )));
231 }
232
233 if !lines[0].starts_with("### Stranded II Mapfile") {
235 return Err(S2Error::InvalidHeader(format!(
236 "not a Stranded II map file: {:?}",
237 lines[0]
238 )));
239 }
240
241 let version = lines.get(1).cloned().unwrap_or_default();
243 let date = lines.get(2).cloned().unwrap_or_default();
244 let time = lines.get(3).cloned().unwrap_or_default();
245 let author = lines.get(4).cloned().unwrap_or_default();
246 let map_type = lines.get(5).cloned().unwrap_or_default();
247
248 Ok(S2Header {
253 version,
254 date,
255 time,
256 author,
257 map_type,
258 })
259}
260
261fn parse_env_vars(c: &mut Cursor<&[u8]>) -> S2Result<S2EnvVars> {
281 let day = read_u32_le(c)?;
282 let hour = read_u8(c)?;
283 let minute = read_u8(c)?;
284 let freezetime = read_u8(c)?;
285 let skybox = read_s2_string(c)?;
286 let multiplayer = read_u8(c)?;
287 let climate = read_u8(c)?;
288 let music = read_s2_string(c)?;
289 let briefing = read_s2_string(c)?;
290 let f_r = read_u8(c)?;
291 let f_g = read_u8(c)?;
292 let f_b = read_u8(c)?;
293 let f_mode = read_u8(c)?;
294 let extra = read_u8(c)?;
295
296 Ok(S2EnvVars {
297 day,
298 hour,
299 minute,
300 freezetime,
301 skybox,
302 multiplayer,
303 climate,
304 music,
305 briefing,
306 fog: [f_r, f_g, f_b, f_mode],
307 extra,
308 })
309}
310
311fn parse_quickslots(c: &mut Cursor<&[u8]>) -> S2Result<Vec<String>> {
316 let mut slots = Vec::with_capacity(10);
317 for _ in 0..10 {
318 slots.push(read_s2_string(c)?);
319 }
320 Ok(slots)
321}
322
323fn parse_footer(
328 c: &mut Cursor<&[u8]>,
329) -> S2Result<(
330 Vec<S2Object>,
331 Vec<S2Unit>,
332 Vec<S2Item>,
333 Vec<S2Info>,
334 Vec<S2State>,
335 Vec<S2Extension>,
336 S2Trailer,
337)> {
338 let obj_count = read_u32_le(c)?;
340 let unit_count = read_u32_le(c)?;
341 let item_count = read_u32_le(c)?;
342 let info_count = read_u32_le(c)?;
343 let state_count = read_u32_le(c)?;
344 let ext_count = read_u32_le(c)?;
345
346 let tf_object = obj_count > 255;
349 let tf_unit = unit_count > 255;
350 let tf_item = item_count > 255;
351
352 let mut objects = Vec::with_capacity(obj_count as usize);
353 for _ in 0..obj_count {
354 objects.push(parse_object(c, tf_object)?);
355 }
356
357 let mut units = Vec::with_capacity(unit_count as usize);
358 for _ in 0..unit_count {
359 units.push(parse_unit(c, tf_unit)?);
360 }
361
362 let mut items = Vec::with_capacity(item_count as usize);
363 for _ in 0..item_count {
364 items.push(parse_item(c, tf_item)?);
365 }
366
367 let mut infos = Vec::with_capacity(info_count as usize);
368 for _ in 0..info_count {
369 infos.push(parse_info(c)?);
370 }
371
372 let mut states = Vec::with_capacity(state_count as usize);
373 for _ in 0..state_count {
374 states.push(parse_state(c)?);
375 }
376
377 let mut extensions = Vec::with_capacity(ext_count as usize);
378 for _ in 0..ext_count {
379 extensions.push(parse_extension(c)?);
380 }
381
382 let blank = read_s2_line(c)?;
384 let eof_marker = read_s2_line(c)?;
385 let url = read_s2_line(c)?;
386
387 if !eof_marker.is_empty() && eof_marker != "### EOF Map File" {
388 return Err(S2Error::InvalidTrailer(format!(
389 "expected '### EOF Map File', got {:?}",
390 eof_marker
391 )));
392 }
393 if !url.is_empty() && url != "www.unrealsoftware.de" {
394 return Err(S2Error::InvalidTrailer(format!(
395 "expected 'www.unrealsoftware.de', got {:?}",
396 url
397 )));
398 }
399
400 let trailer = S2Trailer {
401 blank,
402 eof_marker,
403 url,
404 };
405
406 Ok((objects, units, items, infos, states, extensions, trailer))
407}
408
409fn parse_object(c: &mut Cursor<&[u8]>, tf: bool) -> S2Result<S2Object> {
418 let id = read_u32_le(c)?;
419 let typ = if tf {
420 read_u16_le(c)? as u32
421 } else {
422 read_u8(c)? as u32
423 };
424 let x = read_f32_le(c)?;
425 let z = read_f32_le(c)?;
426 let yaw = read_f32_le(c)?;
427 let health = read_f32_le(c)?;
428 let health_max = read_f32_le(c)?;
429 let day_timer = read_u32_le(c)?;
430 Ok(S2Object {
431 id,
432 typ,
433 x,
434 z,
435 yaw,
436 health,
437 health_max,
438 day_timer,
439 })
440}
441
442fn parse_unit(c: &mut Cursor<&[u8]>, tf: bool) -> S2Result<S2Unit> {
449 let id = read_u32_le(c)?;
450 let typ = if tf {
451 read_u16_le(c)? as u32
452 } else {
453 read_u8(c)? as u32
454 };
455 let x = read_f32_le(c)?;
456 let y = read_f32_le(c)?;
457 let z = read_f32_le(c)?;
458 let yaw = read_f32_le(c)?;
459 let health = read_f32_le(c)?;
460 let health_max = read_f32_le(c)?;
461 let hunger = read_f32_le(c)?;
462 let thirst = read_f32_le(c)?;
463 let exhaustion = read_f32_le(c)?;
464 let ai_center_x = read_f32_le(c)?;
465 let ai_center_z = read_f32_le(c)?;
466
467 Ok(S2Unit {
468 id,
469 typ,
470 x,
471 y,
472 z,
473 yaw,
474 health,
475 health_max,
476 hunger,
477 thirst,
478 exhaustion,
479 ai_center_x,
480 ai_center_z,
481 })
482}
483
484fn parse_item(c: &mut Cursor<&[u8]>, tf: bool) -> S2Result<S2Item> {
490 let id = read_u32_le(c)?;
491 let typ = if tf {
492 read_u16_le(c)? as u32
493 } else {
494 read_u8(c)? as u32
495 };
496 let x = read_f32_le(c)?;
497 let y = read_f32_le(c)?;
498 let z = read_f32_le(c)?;
499 let yaw = read_f32_le(c)?;
500 let health = read_f32_le(c)?;
501 let count = read_u32_le(c)?;
502 let parent_class = read_u8(c)?;
503 let parent_mode = read_u8(c)?;
504 let parent_id = read_u32_le(c)?;
505 Ok(S2Item {
506 id,
507 typ,
508 x,
509 y,
510 z,
511 yaw,
512 health,
513 count,
514 parent_class,
515 parent_mode,
516 parent_id,
517 })
518}
519
520fn parse_info(c: &mut Cursor<&[u8]>) -> S2Result<S2Info> {
526 let id = read_u32_le(c)?;
527 let typ = read_u8(c)?;
528 let x = read_f32_le(c)?;
529 let y = read_f32_le(c)?;
530 let z = read_f32_le(c)?;
531 let pitch = read_f32_le(c)?;
532 let yaw = read_f32_le(c)?;
533 let vars = read_s2_string(c)?;
534 Ok(S2Info {
535 id,
536 typ,
537 x,
538 y,
539 z,
540 pitch,
541 yaw,
542 vars,
543 })
544}
545
546fn parse_state(c: &mut Cursor<&[u8]>) -> S2Result<S2State> {
552 let typ = read_u8(c)? as u32;
553 let parent_class = read_u8(c)? as u32;
554 let parent_id = read_u32_le(c)?;
555 let x = read_f32_le(c)?;
556 let y = read_f32_le(c)?;
557 let z = read_f32_le(c)?;
558 let fx = read_f32_le(c)?;
559 let fy = read_f32_le(c)?;
560 let fz = read_f32_le(c)?;
561 let value = read_u32_le(c)?;
562 let value_f = read_f32_le(c)?;
563 let value_s = read_s2_string(c)?;
564 Ok(S2State {
565 typ,
566 parent_class,
567 parent_id,
568 x,
569 y,
570 z,
571 fx,
572 fy,
573 fz,
574 value,
575 value_f,
576 value_s,
577 })
578}
579
580fn parse_extension(c: &mut Cursor<&[u8]>) -> S2Result<S2Extension> {
586 let typ = read_u8(c)?;
587 let parent_class = read_u8(c)?;
588 let parent_id = read_u32_le(c)?;
589 let mode = read_u32_le(c)?;
590 let key = if mode == 0 {
591 let _empty = read_s2_string(c)?;
593 String::new()
594 } else {
595 read_s2_string(c)?
596 };
597 let value = read_s2_string(c)?;
598 let stuff = read_s2_string(c)?;
599 Ok(S2Extension {
600 typ,
601 parent_class,
602 parent_id,
603 mode,
604 key,
605 value,
606 stuff,
607 })
608}
609
610pub fn parse_s2_file(path: impl AsRef<std::path::Path>) -> S2Result<S2Map> {
616 let data = std::fs::read(path.as_ref()).map_err(S2Error::Io)?;
617 parse_s2(&data)
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623
624 #[test]
625 fn test_read_s2_string_empty() {
626 let data: &[u8] = &[0, 0, 0, 0];
627 let mut c = Cursor::new(data);
628 let s = read_s2_string(&mut c).unwrap();
629 assert_eq!(s, "");
630 assert_eq!(c.position(), 4);
631 }
632
633 #[test]
634 fn test_read_s2_string_hello() {
635 let data: &[u8] = &[5, 0, 0, 0, b'H', b'e', b'l', b'l', b'o'];
636 let mut c = Cursor::new(data);
637 let s = read_s2_string(&mut c).unwrap();
638 assert_eq!(s, "Hello");
639 }
640
641 #[test]
642 fn test_parse_quickslots() {
643 let mut data = Vec::new();
644 for _ in 0..10 {
645 data.extend_from_slice(&[1, 0, 0, 0, b'0']);
646 }
647 let mut c = Cursor::new(data.as_slice());
648 let slots = parse_quickslots(&mut c).unwrap();
649 assert_eq!(slots.len(), 10);
650 assert!(slots.iter().all(|s| s == "0"));
651 }
652
653 #[test]
654 fn test_decode_password_empty() {
655 let pw = decode_password("", 0);
656 assert_eq!(pw, "");
657 }
658
659 #[test]
660 fn test_decode_password_xor() {
661 let encoded = "\x03\x00\x01";
666 let pw = decode_password(encoded, 50);
667 assert_eq!(pw, "123");
668 }
669
670 fn build_minimal_s2() -> Vec<u8> {
673 let mut buf = Vec::new();
674
675 buf.extend_from_slice(b"### Stranded II Mapfile \xa9by Unreal Software 2004-2007\r\n");
678 buf.extend_from_slice(b"1.0.0.1\r\n");
679 buf.extend_from_slice(b"10 Jul 2026\r\n");
680 buf.extend_from_slice(b"18:10:56\r\n");
681 buf.extend_from_slice(b"default\r\n");
682 buf.extend_from_slice(b"map\r\n");
683 buf.extend_from_slice(b"\r\n");
684 buf.extend_from_slice(b"\r\n");
685 buf.extend_from_slice(b"\r\n");
686 buf.extend_from_slice(b"\r\n");
687 buf.extend_from_slice(b"\r\n");
688 buf.extend_from_slice(b"###\r\n");
689
690 buf.extend(std::iter::repeat(0u8).take(20736));
692
693 buf.push(0); buf.extend_from_slice(b"\r\n"); buf.extend_from_slice(&1u32.to_le_bytes()); buf.push(9); buf.push(0); buf.push(3); buf.extend_from_slice(&3u32.to_le_bytes()); buf.extend_from_slice(b"sky");
704 buf.push(0); buf.push(0); let music = b"amb_jungle.mp3";
707 buf.extend_from_slice(&(music.len() as u32).to_le_bytes());
708 buf.extend_from_slice(music);
709 buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&[205, 210, 245, 0]); buf.push(0); for _ in 0..10 {
715 buf.extend_from_slice(&[1, 0, 0, 0, b'0']);
716 }
717
718 buf.extend_from_slice(&128u32.to_le_bytes());
720 buf.extend(std::iter::repeat(0u8).take(128 * 128 * 3));
721
722 buf.extend_from_slice(&16u32.to_le_bytes());
724 for _ in 0..289 {
725 buf.extend_from_slice(&0.447_f32.to_le_bytes());
726 }
727
728 buf.extend(std::iter::repeat(0u8).take(16641));
730
731 buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&1u32.to_le_bytes()); buf.push(0); buf.push(0); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&6u32.to_le_bytes()); buf.extend_from_slice(&8u32.to_le_bytes()); buf.extend_from_slice(b"lasttime");
747 buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&5u32.to_le_bytes()); buf.extend_from_slice(b"1,9,2"); buf.extend_from_slice(b"\r\n");
753 buf.extend_from_slice(b"### EOF Map File\r\n");
754 buf.extend_from_slice(b"www.unrealsoftware.de\r\n");
755
756 buf
757 }
758
759 #[test]
760 fn test_parse_minimal_s2() {
761 let data = build_minimal_s2();
762 let result = parse_s2(&data);
763 assert!(result.is_ok(), "parse failed: {:?}", result.err());
764 let map = result.unwrap();
765 assert_eq!(map.header.version, "1.0.0.1");
766 assert_eq!(map.terrain_size, 16);
767 assert!(map.objects.is_empty());
768 assert!(map.units.is_empty());
769 assert!(map.items.is_empty());
770 assert!(map.infos.is_empty());
771 assert!(map.states.is_empty());
772 assert_eq!(map.extensions.len(), 1);
773 assert_eq!(map.extensions[0].key, "lasttime");
774 }
775
776 #[test]
777 fn test_parse_minimal_s2_with_password() {
778 let mut buf = Vec::new();
779
780 buf.extend_from_slice(b"### Stranded II Mapfile \xa9by Unreal Software 2004-2007\r\n");
782 buf.extend_from_slice(b"1.0.0.1\r\n");
783 buf.extend_from_slice(b"10 Jul 2026\r\n");
784 buf.extend_from_slice(b"18:10:56\r\n");
785 buf.extend_from_slice(b"default\r\n");
786 buf.extend_from_slice(b"map\r\n");
787 buf.extend_from_slice(b"\r\n");
788 buf.extend_from_slice(b"\r\n");
789 buf.extend_from_slice(b"\r\n");
790 buf.extend_from_slice(b"\r\n");
791 buf.extend_from_slice(b"\r\n");
792 buf.extend_from_slice(b"###\r\n");
793
794 buf.extend(std::iter::repeat(0u8).take(20736));
796
797 buf.push(50); buf.extend_from_slice(b"\x03\x00\x01\r\n"); buf.extend_from_slice(&1u32.to_le_bytes()); buf.push(9);
805 buf.push(0);
806 buf.push(3);
807 buf.extend_from_slice(&3u32.to_le_bytes());
808 buf.extend_from_slice(b"sky");
809 buf.push(0);
810 buf.push(0);
811 let music = b"amb_jungle.mp3";
812 buf.extend_from_slice(&(music.len() as u32).to_le_bytes());
813 buf.extend_from_slice(music);
814 buf.extend_from_slice(&0u32.to_le_bytes());
815 buf.extend_from_slice(&[205, 210, 245, 0]);
816 buf.push(0);
817 for _ in 0..10 {
818 buf.extend_from_slice(&[1, 0, 0, 0, b'0']);
819 }
820 buf.extend_from_slice(&128u32.to_le_bytes());
821 buf.extend(std::iter::repeat(0u8).take(128 * 128 * 3));
822 buf.extend_from_slice(&16u32.to_le_bytes());
823 for _ in 0..289 {
824 buf.extend_from_slice(&0.447_f32.to_le_bytes());
825 }
826 buf.extend(std::iter::repeat(0u8).take(16641));
827 buf.extend_from_slice(&0u32.to_le_bytes());
828 buf.extend_from_slice(&0u32.to_le_bytes());
829 buf.extend_from_slice(&0u32.to_le_bytes());
830 buf.extend_from_slice(&0u32.to_le_bytes());
831 buf.extend_from_slice(&0u32.to_le_bytes());
832 buf.extend_from_slice(&1u32.to_le_bytes());
833 buf.push(0);
834 buf.push(0);
835 buf.extend_from_slice(&0u32.to_le_bytes());
836 buf.extend_from_slice(&6u32.to_le_bytes());
837 buf.extend_from_slice(&8u32.to_le_bytes());
838 buf.extend_from_slice(b"lasttime");
839 buf.extend_from_slice(&0u32.to_le_bytes());
840 buf.extend_from_slice(&5u32.to_le_bytes());
841 buf.extend_from_slice(b"1,9,2");
842 buf.extend_from_slice(b"\r\n");
843 buf.extend_from_slice(b"### EOF Map File\r\n");
844 buf.extend_from_slice(b"www.unrealsoftware.de\r\n");
845
846 let result = parse_s2(&buf);
847 assert!(result.is_ok(), "parse failed: {:?}", result.err());
848 let map = result.unwrap();
849 assert_eq!(map.password.key, 50);
850 assert_eq!(map.password.decoded, "123");
851 }
852}