1use std::io::{Cursor, Write};
4
5use crate::binary;
6
7use super::{Lyt, LytError};
8
9const LYT_LINE_SEP: &str = "\r\n";
10const LYT_INDENT: &str = " ";
11
12#[cfg_attr(
14 feature = "tracing",
15 tracing::instrument(level = "debug", skip(writer, lyt))
16)]
17pub fn write_lyt<W: Write>(writer: &mut W, lyt: &Lyt) -> Result<(), LytError> {
18 write_cp1252(
19 writer,
20 &format!("beginlayout{LYT_LINE_SEP}"),
21 "header".into(),
22 )?;
23
24 write_cp1252(
25 writer,
26 &format!("{LYT_INDENT}roomcount {}{LYT_LINE_SEP}", lyt.rooms.len()),
27 "roomcount".into(),
28 )?;
29 for room in &lyt.rooms {
30 validate_name_token("room.model", &room.model)?;
31 write_cp1252(
32 writer,
33 &format!(
34 "{LYT_INDENT}{LYT_INDENT}{} {} {} {}{LYT_LINE_SEP}",
35 room.model,
36 fmt_f32(room.position.x),
37 fmt_f32(room.position.y),
38 fmt_f32(room.position.z)
39 ),
40 format!("room `{}`", room.model),
41 )?;
42 }
43
44 write_cp1252(
45 writer,
46 &format!("{LYT_INDENT}trackcount {}{LYT_LINE_SEP}", lyt.tracks.len()),
47 "trackcount".into(),
48 )?;
49 for track in &lyt.tracks {
50 validate_name_token("track.model", &track.model)?;
51 write_cp1252(
52 writer,
53 &format!(
54 "{LYT_INDENT}{LYT_INDENT}{} {} {} {}{LYT_LINE_SEP}",
55 track.model,
56 fmt_f32(track.position.x),
57 fmt_f32(track.position.y),
58 fmt_f32(track.position.z)
59 ),
60 format!("track `{}`", track.model),
61 )?;
62 }
63
64 write_cp1252(
65 writer,
66 &format!(
67 "{LYT_INDENT}obstaclecount {}{LYT_LINE_SEP}",
68 lyt.obstacles.len()
69 ),
70 "obstaclecount".into(),
71 )?;
72 for obstacle in &lyt.obstacles {
73 validate_name_token("obstacle.model", &obstacle.model)?;
74 write_cp1252(
75 writer,
76 &format!(
77 "{LYT_INDENT}{LYT_INDENT}{} {} {} {}{LYT_LINE_SEP}",
78 obstacle.model,
79 fmt_f32(obstacle.position.x),
80 fmt_f32(obstacle.position.y),
81 fmt_f32(obstacle.position.z)
82 ),
83 format!("obstacle `{}`", obstacle.model),
84 )?;
85 }
86
87 write_cp1252(
88 writer,
89 &format!(
90 "{LYT_INDENT}doorhookcount {}{LYT_LINE_SEP}",
91 lyt.doorhooks.len()
92 ),
93 "doorhookcount".into(),
94 )?;
95 for doorhook in &lyt.doorhooks {
96 validate_name_token("doorhook.room", &doorhook.room)?;
97 validate_name_token("doorhook.door", &doorhook.door)?;
98 write_cp1252(
99 writer,
100 &format!(
101 "{LYT_INDENT}{LYT_INDENT}{} {} 0 {} {} {} {} {} {} {}{LYT_LINE_SEP}",
102 doorhook.room,
103 doorhook.door,
104 fmt_f32(doorhook.position.x),
105 fmt_f32(doorhook.position.y),
106 fmt_f32(doorhook.position.z),
107 fmt_f32(doorhook.orientation.x),
108 fmt_f32(doorhook.orientation.y),
109 fmt_f32(doorhook.orientation.z),
110 fmt_f32(doorhook.orientation.w)
111 ),
112 format!("doorhook `{}`/`{}`", doorhook.room, doorhook.door),
113 )?;
114 }
115
116 write_cp1252(writer, "donelayout", "footer".into())?;
117 Ok(())
118}
119
120#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(lyt)))]
122pub fn write_lyt_to_vec(lyt: &Lyt) -> Result<Vec<u8>, LytError> {
123 let mut cursor = Cursor::new(Vec::new());
124 write_lyt(&mut cursor, lyt)?;
125 Ok(cursor.into_inner())
126}
127
128fn validate_name_token(field: &'static str, value: &str) -> Result<(), LytError> {
129 if value.is_empty() || value.chars().any(char::is_whitespace) {
130 return Err(LytError::InvalidName {
131 field,
132 value: value.to_string(),
133 });
134 }
135 Ok(())
136}
137
138fn fmt_f32(v: f32) -> String {
149 let s = format!("{v}");
150 if s.contains('.') || s.contains('e') || s.contains('E') {
151 s
152 } else {
153 format!("{s}.0")
154 }
155}
156
157fn write_cp1252<W: Write>(writer: &mut W, text: &str, context: String) -> Result<(), LytError> {
158 binary::write_cp1252(writer, text, context, |context, source| {
159 LytError::TextEncoding { context, source }
160 })
161}