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 tf = TypeFormat::from_string(&header.type_format);
179 let (objects, units, items, infos, states, extensions, trailer) = parse_footer(&mut c, tf)?;
180
181 let decoded_pw = decode_password(&encoded_pw, pw_key);
183
184 Ok(S2Map {
185 header,
186 minimap,
187 password: S2Password {
188 key: pw_key,
189 encoded: encoded_pw,
190 decoded: decoded_pw,
191 },
192 env_vars,
193 quickslots,
194 colormap_dim,
195 colormap,
196 terrain_size,
197 heights,
198 grass,
199 objects,
200 units,
201 items,
202 infos,
203 states,
204 extensions,
205 trailer,
206 })
207}
208
209fn decode_password(encoded: &str, key: u8) -> String {
211 let bytes = encoded.as_bytes();
212 let decoded: Vec<u8> = bytes.iter().map(|&b| b ^ key).collect();
213 String::from_utf8_lossy(&decoded).into_owned()
214}
215
216fn parse_header(c: &mut Cursor<&[u8]>) -> S2Result<S2Header> {
221 let mut lines: Vec<String> = Vec::new();
222 loop {
223 let line = read_s2_line(c)?;
224 if line == "###" {
225 break;
226 }
227 lines.push(line);
228 }
229
230 if lines.len() < 11 {
231 return Err(S2Error::InvalidHeader(format!(
232 "expected 11+ header lines before '###', got {}",
233 lines.len()
234 )));
235 }
236
237 if !lines[0].starts_with("### Stranded II Mapfile") {
239 return Err(S2Error::InvalidHeader(format!(
240 "not a Stranded II map file: {:?}",
241 lines[0]
242 )));
243 }
244
245 let version = lines.get(1).cloned().unwrap_or_default();
247 let date = lines.get(2).cloned().unwrap_or_default();
248 let time = lines.get(3).cloned().unwrap_or_default();
249 let author = lines.get(4).cloned().unwrap_or_default();
250 let map_type = lines.get(5).cloned().unwrap_or_default();
251
252 let type_format = lines.get(6).cloned().unwrap_or_default();
254
255 Ok(S2Header {
258 version,
259 date,
260 time,
261 author,
262 map_type,
263 type_format,
264 })
265}
266
267fn parse_env_vars(c: &mut Cursor<&[u8]>) -> S2Result<S2EnvVars> {
287 let day = read_u32_le(c)?;
288 let hour = read_u8(c)?;
289 let minute = read_u8(c)?;
290 let freezetime = read_u8(c)?;
291 let skybox = read_s2_string(c)?;
292 let multiplayer = read_u8(c)?;
293 let climate = read_u8(c)?;
294 let music = read_s2_string(c)?;
295 let briefing = read_s2_string(c)?;
296 let f_r = read_u8(c)?;
297 let f_g = read_u8(c)?;
298 let f_b = read_u8(c)?;
299 let f_mode = read_u8(c)?;
300 let extra = read_u8(c)?;
301
302 Ok(S2EnvVars {
303 day,
304 hour,
305 minute,
306 freezetime,
307 skybox,
308 multiplayer,
309 climate,
310 music,
311 briefing,
312 fog: [f_r, f_g, f_b, f_mode],
313 extra,
314 })
315}
316
317fn parse_quickslots(c: &mut Cursor<&[u8]>) -> S2Result<Vec<String>> {
322 let mut slots = Vec::with_capacity(10);
323 for _ in 0..10 {
324 slots.push(read_s2_string(c)?);
325 }
326 Ok(slots)
327}
328
329#[derive(Debug, Clone, Copy)]
336struct TypeFormat {
337 tf_object: bool,
338 tf_unit: bool,
339 tf_item: bool,
340}
341
342impl TypeFormat {
343 fn from_string(s: &str) -> Self {
346 let chars: Vec<char> = s.chars().collect();
347 TypeFormat {
348 tf_object: chars.first().copied() == Some('1'),
349 tf_unit: chars.get(1).copied() == Some('1'),
350 tf_item: chars.get(2).copied() == Some('1'),
351 }
352 }
353
354}
355
356fn parse_footer(
379 c: &mut Cursor<&[u8]>,
380 tf: TypeFormat,
381) -> S2Result<(
382 Vec<S2Object>,
383 Vec<S2Unit>,
384 Vec<S2Item>,
385 Vec<S2Info>,
386 Vec<S2State>,
387 Vec<S2Extension>,
388 S2Trailer,
389)> {
390 let obj_count = read_u32_le(c)?;
392 let mut objects = Vec::with_capacity(obj_count as usize);
393 for _ in 0..obj_count {
394 objects.push(parse_object(c, tf.tf_object)?);
395 }
396
397 let unit_count = read_u32_le(c)?;
399 let mut units = Vec::with_capacity(unit_count as usize);
400 for _ in 0..unit_count {
401 units.push(parse_unit(c, tf.tf_unit)?);
402 }
403
404 let item_count = read_u32_le(c)?;
406 let mut items = Vec::with_capacity(item_count as usize);
407 for _ in 0..item_count {
408 items.push(parse_item(c, tf.tf_item)?);
409 }
410
411 let info_count = read_u32_le(c)?;
413 let mut infos = Vec::with_capacity(info_count as usize);
414 for _ in 0..info_count {
415 infos.push(parse_info(c)?);
416 }
417
418 let state_count = read_u32_le(c)?;
420 let mut states = Vec::with_capacity(state_count as usize);
421 for _ in 0..state_count {
422 states.push(parse_state(c)?);
423 }
424
425 let ext_count = read_u32_le(c)?;
427 let mut extensions = Vec::with_capacity(ext_count as usize);
428 for _ in 0..ext_count {
429 extensions.push(parse_extension(c)?);
430 }
431
432 let blank = read_s2_line(c)?;
434 let eof_marker = read_s2_line(c)?;
435 let url = read_s2_line(c)?;
436
437 if !eof_marker.is_empty() && eof_marker != "### EOF Map File" {
438 return Err(S2Error::InvalidTrailer(format!(
439 "expected '### EOF Map File', got {:?}",
440 eof_marker
441 )));
442 }
443 if !url.is_empty() && url != "www.unrealsoftware.de" {
444 return Err(S2Error::InvalidTrailer(format!(
445 "expected 'www.unrealsoftware.de', got {:?}",
446 url
447 )));
448 }
449
450 let trailer = S2Trailer {
451 blank,
452 eof_marker,
453 url,
454 };
455
456 Ok((objects, units, items, infos, states, extensions, trailer))
457}
458
459fn parse_object(c: &mut Cursor<&[u8]>, tf: bool) -> S2Result<S2Object> {
468 let id = read_u32_le(c)?;
469 let typ = if tf {
470 read_u16_le(c)? as u32
471 } else {
472 read_u8(c)? as u32
473 };
474 let x = read_f32_le(c)?;
475 let z = read_f32_le(c)?;
476 let yaw = read_f32_le(c)?;
477 let health = read_f32_le(c)?;
478 let health_max = read_f32_le(c)?;
479 let day_timer = read_u32_le(c)?;
480 Ok(S2Object {
481 id,
482 typ,
483 x,
484 z,
485 yaw,
486 health,
487 health_max,
488 day_timer,
489 })
490}
491
492fn parse_unit(c: &mut Cursor<&[u8]>, tf: bool) -> S2Result<S2Unit> {
499 let id = read_u32_le(c)?;
500 let typ = if tf {
501 read_u16_le(c)? as u32
502 } else {
503 read_u8(c)? as u32
504 };
505 let x = read_f32_le(c)?;
506 let y = read_f32_le(c)?;
507 let z = read_f32_le(c)?;
508 let yaw = read_f32_le(c)?;
509 let health = read_f32_le(c)?;
510 let health_max = read_f32_le(c)?;
511 let hunger = read_f32_le(c)?;
512 let thirst = read_f32_le(c)?;
513 let exhaustion = read_f32_le(c)?;
514 let ai_center_x = read_f32_le(c)?;
515 let ai_center_z = read_f32_le(c)?;
516
517 Ok(S2Unit {
518 id,
519 typ,
520 x,
521 y,
522 z,
523 yaw,
524 health,
525 health_max,
526 hunger,
527 thirst,
528 exhaustion,
529 ai_center_x,
530 ai_center_z,
531 })
532}
533
534fn parse_item(c: &mut Cursor<&[u8]>, tf: bool) -> S2Result<S2Item> {
540 let id = read_u32_le(c)?;
541 let typ = if tf {
542 read_u16_le(c)? as u32
543 } else {
544 read_u8(c)? as u32
545 };
546 let x = read_f32_le(c)?;
547 let y = read_f32_le(c)?;
548 let z = read_f32_le(c)?;
549 let yaw = read_f32_le(c)?;
550 let health = read_f32_le(c)?;
551 let count = read_u32_le(c)?;
552 let parent_class = read_u8(c)?;
553 let parent_mode = read_u8(c)?;
554 let parent_id = read_u32_le(c)?;
555 Ok(S2Item {
556 id,
557 typ,
558 x,
559 y,
560 z,
561 yaw,
562 health,
563 count,
564 parent_class,
565 parent_mode,
566 parent_id,
567 })
568}
569
570fn parse_info(c: &mut Cursor<&[u8]>) -> S2Result<S2Info> {
576 let id = read_u32_le(c)?;
577 let typ = read_u8(c)?;
578 let x = read_f32_le(c)?;
579 let y = read_f32_le(c)?;
580 let z = read_f32_le(c)?;
581 let pitch = read_f32_le(c)?;
582 let yaw = read_f32_le(c)?;
583 let vars = read_s2_string(c)?;
584 Ok(S2Info {
585 id,
586 typ,
587 x,
588 y,
589 z,
590 pitch,
591 yaw,
592 vars,
593 })
594}
595
596fn parse_state(c: &mut Cursor<&[u8]>) -> S2Result<S2State> {
602 let typ = read_u8(c)? as u32;
603 let parent_class = read_u8(c)? as u32;
604 let parent_id = read_u32_le(c)?;
605 let x = read_f32_le(c)?;
606 let y = read_f32_le(c)?;
607 let z = read_f32_le(c)?;
608 let fx = read_f32_le(c)?;
609 let fy = read_f32_le(c)?;
610 let fz = read_f32_le(c)?;
611 let value = read_u32_le(c)?;
612 let value_f = read_f32_le(c)?;
613 let value_s = read_s2_string(c)?;
614 Ok(S2State {
615 typ,
616 parent_class,
617 parent_id,
618 x,
619 y,
620 z,
621 fx,
622 fy,
623 fz,
624 value,
625 value_f,
626 value_s,
627 })
628}
629
630fn parse_extension(c: &mut Cursor<&[u8]>) -> S2Result<S2Extension> {
636 let typ = read_u8(c)?;
637 let parent_class = read_u8(c)?;
638 let parent_id = read_u32_le(c)?;
639 let mode = read_u32_le(c)?;
640 let key = if mode == 0 {
641 let _empty = read_s2_string(c)?;
643 String::new()
644 } else {
645 read_s2_string(c)?
646 };
647 let value = read_s2_string(c)?;
648 let stuff = read_s2_string(c)?;
649 Ok(S2Extension {
650 typ,
651 parent_class,
652 parent_id,
653 mode,
654 key,
655 value,
656 stuff,
657 })
658}
659
660pub fn parse_s2_file(path: impl AsRef<std::path::Path>) -> S2Result<S2Map> {
666 let data = std::fs::read(path.as_ref()).map_err(S2Error::Io)?;
667 parse_s2(&data)
668}
669
670#[cfg(test)]
671mod tests {
672 use super::*;
673
674 #[test]
675 fn test_read_s2_string_empty() {
676 let data: &[u8] = &[0, 0, 0, 0];
677 let mut c = Cursor::new(data);
678 let s = read_s2_string(&mut c).unwrap();
679 assert_eq!(s, "");
680 assert_eq!(c.position(), 4);
681 }
682
683 #[test]
684 fn test_read_s2_string_hello() {
685 let data: &[u8] = &[5, 0, 0, 0, b'H', b'e', b'l', b'l', b'o'];
686 let mut c = Cursor::new(data);
687 let s = read_s2_string(&mut c).unwrap();
688 assert_eq!(s, "Hello");
689 }
690
691 #[test]
692 fn test_parse_quickslots() {
693 let mut data = Vec::new();
694 for _ in 0..10 {
695 data.extend_from_slice(&[1, 0, 0, 0, b'0']);
696 }
697 let mut c = Cursor::new(data.as_slice());
698 let slots = parse_quickslots(&mut c).unwrap();
699 assert_eq!(slots.len(), 10);
700 assert!(slots.iter().all(|s| s == "0"));
701 }
702
703 #[test]
704 fn test_decode_password_empty() {
705 let pw = decode_password("", 0);
706 assert_eq!(pw, "");
707 }
708
709 #[test]
710 fn test_decode_password_xor() {
711 let encoded = "\x03\x00\x01";
716 let pw = decode_password(encoded, 50);
717 assert_eq!(pw, "123");
718 }
719
720 fn build_minimal_s2() -> Vec<u8> {
723 let mut buf = Vec::new();
724
725 buf.extend_from_slice(b"### Stranded II Mapfile \xa9by Unreal Software 2004-2007\r\n");
728 buf.extend_from_slice(b"1.0.0.1\r\n");
729 buf.extend_from_slice(b"10 Jul 2026\r\n");
730 buf.extend_from_slice(b"18:10:56\r\n");
731 buf.extend_from_slice(b"default\r\n");
732 buf.extend_from_slice(b"map\r\n");
733 buf.extend_from_slice(b"\r\n");
734 buf.extend_from_slice(b"\r\n");
735 buf.extend_from_slice(b"\r\n");
736 buf.extend_from_slice(b"\r\n");
737 buf.extend_from_slice(b"\r\n");
738 buf.extend_from_slice(b"###\r\n");
739
740 buf.extend(std::iter::repeat(0u8).take(20736));
742
743 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");
754 buf.push(0); buf.push(0); let music = b"amb_jungle.mp3";
757 buf.extend_from_slice(&(music.len() as u32).to_le_bytes());
758 buf.extend_from_slice(music);
759 buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&[205, 210, 245, 0]); buf.push(0); for _ in 0..10 {
765 buf.extend_from_slice(&[1, 0, 0, 0, b'0']);
766 }
767
768 buf.extend_from_slice(&128u32.to_le_bytes());
770 buf.extend(std::iter::repeat(0u8).take(128 * 128 * 3));
771
772 buf.extend_from_slice(&16u32.to_le_bytes());
774 for _ in 0..289 {
775 buf.extend_from_slice(&0.447_f32.to_le_bytes());
776 }
777
778 buf.extend(std::iter::repeat(0u8).take(16641));
780
781 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");
797 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");
803 buf.extend_from_slice(b"### EOF Map File\r\n");
804 buf.extend_from_slice(b"www.unrealsoftware.de\r\n");
805
806 buf
807 }
808
809 #[test]
810 fn test_parse_minimal_s2() {
811 let data = build_minimal_s2();
812 let result = parse_s2(&data);
813 assert!(result.is_ok(), "parse failed: {:?}", result.err());
814 let map = result.unwrap();
815 assert_eq!(map.header.version, "1.0.0.1");
816 assert_eq!(map.terrain_size, 16);
817 assert!(map.objects.is_empty());
818 assert!(map.units.is_empty());
819 assert!(map.items.is_empty());
820 assert!(map.infos.is_empty());
821 assert!(map.states.is_empty());
822 assert_eq!(map.extensions.len(), 1);
823 assert_eq!(map.extensions[0].key, "lasttime");
824 }
825
826 #[test]
827 fn test_parse_minimal_s2_with_password() {
828 let mut buf = Vec::new();
829
830 buf.extend_from_slice(b"### Stranded II Mapfile \xa9by Unreal Software 2004-2007\r\n");
832 buf.extend_from_slice(b"1.0.0.1\r\n");
833 buf.extend_from_slice(b"10 Jul 2026\r\n");
834 buf.extend_from_slice(b"18:10:56\r\n");
835 buf.extend_from_slice(b"default\r\n");
836 buf.extend_from_slice(b"map\r\n");
837 buf.extend_from_slice(b"\r\n");
838 buf.extend_from_slice(b"\r\n");
839 buf.extend_from_slice(b"\r\n");
840 buf.extend_from_slice(b"\r\n");
841 buf.extend_from_slice(b"\r\n");
842 buf.extend_from_slice(b"###\r\n");
843
844 buf.extend(std::iter::repeat(0u8).take(20736));
846
847 buf.push(50); buf.extend_from_slice(b"\x03\x00\x01\r\n"); buf.extend_from_slice(&1u32.to_le_bytes()); buf.push(9);
855 buf.push(0);
856 buf.push(3);
857 buf.extend_from_slice(&3u32.to_le_bytes());
858 buf.extend_from_slice(b"sky");
859 buf.push(0);
860 buf.push(0);
861 let music = b"amb_jungle.mp3";
862 buf.extend_from_slice(&(music.len() as u32).to_le_bytes());
863 buf.extend_from_slice(music);
864 buf.extend_from_slice(&0u32.to_le_bytes());
865 buf.extend_from_slice(&[205, 210, 245, 0]);
866 buf.push(0);
867 for _ in 0..10 {
868 buf.extend_from_slice(&[1, 0, 0, 0, b'0']);
869 }
870 buf.extend_from_slice(&128u32.to_le_bytes());
871 buf.extend(std::iter::repeat(0u8).take(128 * 128 * 3));
872 buf.extend_from_slice(&16u32.to_le_bytes());
873 for _ in 0..289 {
874 buf.extend_from_slice(&0.447_f32.to_le_bytes());
875 }
876 buf.extend(std::iter::repeat(0u8).take(16641));
877 buf.extend_from_slice(&0u32.to_le_bytes());
878 buf.extend_from_slice(&0u32.to_le_bytes());
879 buf.extend_from_slice(&0u32.to_le_bytes());
880 buf.extend_from_slice(&0u32.to_le_bytes());
881 buf.extend_from_slice(&0u32.to_le_bytes());
882 buf.extend_from_slice(&1u32.to_le_bytes());
883 buf.push(0);
884 buf.push(0);
885 buf.extend_from_slice(&0u32.to_le_bytes());
886 buf.extend_from_slice(&6u32.to_le_bytes());
887 buf.extend_from_slice(&8u32.to_le_bytes());
888 buf.extend_from_slice(b"lasttime");
889 buf.extend_from_slice(&0u32.to_le_bytes());
890 buf.extend_from_slice(&5u32.to_le_bytes());
891 buf.extend_from_slice(b"1,9,2");
892 buf.extend_from_slice(b"\r\n");
893 buf.extend_from_slice(b"### EOF Map File\r\n");
894 buf.extend_from_slice(b"www.unrealsoftware.de\r\n");
895
896 let result = parse_s2(&buf);
897 assert!(result.is_ok(), "parse failed: {:?}", result.err());
898 let map = result.unwrap();
899 assert_eq!(map.password.key, 50);
900 assert_eq!(map.password.decoded, "123");
901 }
902}