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(
21 feature = "tracing",
22 tracing::instrument(level = "debug", skip(writer, lyt))
23)]
24pub fn write_lyt<W: Write>(writer: &mut W, lyt: &Lyt) -> Result<(), LytError> {
25 write_cp1252(
26 writer,
27 &format!("beginlayout{LYT_LINE_SEP}"),
28 "header".into(),
29 )?;
30
31 write_cp1252(
32 writer,
33 &format!("{LYT_INDENT}roomcount {}{LYT_LINE_SEP}", lyt.rooms.len()),
34 "roomcount".into(),
35 )?;
36 for room in &lyt.rooms {
37 validate_name_token("room.model", &room.model)?;
38 write_cp1252(
39 writer,
40 &format!(
41 "{LYT_INDENT}{LYT_INDENT}{} {} {} {}{LYT_LINE_SEP}",
42 room.model,
43 fmt_f32(room.position.x),
44 fmt_f32(room.position.y),
45 fmt_f32(room.position.z)
46 ),
47 format!("room `{}`", room.model),
48 )?;
49 }
50
51 write_cp1252(
52 writer,
53 &format!("{LYT_INDENT}trackcount {}{LYT_LINE_SEP}", lyt.tracks.len()),
54 "trackcount".into(),
55 )?;
56 for track in &lyt.tracks {
57 validate_name_token("track.model", &track.model)?;
58 write_cp1252(
59 writer,
60 &format!(
61 "{LYT_INDENT}{LYT_INDENT}{} {} {} {}{LYT_LINE_SEP}",
62 track.model,
63 fmt_f32(track.position.x),
64 fmt_f32(track.position.y),
65 fmt_f32(track.position.z)
66 ),
67 format!("track `{}`", track.model),
68 )?;
69 }
70
71 write_cp1252(
72 writer,
73 &format!(
74 "{LYT_INDENT}obstaclecount {}{LYT_LINE_SEP}",
75 lyt.obstacles.len()
76 ),
77 "obstaclecount".into(),
78 )?;
79 for obstacle in &lyt.obstacles {
80 validate_name_token("obstacle.model", &obstacle.model)?;
81 write_cp1252(
82 writer,
83 &format!(
84 "{LYT_INDENT}{LYT_INDENT}{} {} {} {}{LYT_LINE_SEP}",
85 obstacle.model,
86 fmt_f32(obstacle.position.x),
87 fmt_f32(obstacle.position.y),
88 fmt_f32(obstacle.position.z)
89 ),
90 format!("obstacle `{}`", obstacle.model),
91 )?;
92 }
93
94 write_cp1252(
95 writer,
96 &format!(
97 "{LYT_INDENT}doorhookcount {}{LYT_LINE_SEP}",
98 lyt.doorhooks.len()
99 ),
100 "doorhookcount".into(),
101 )?;
102 for doorhook in &lyt.doorhooks {
103 validate_name_token("doorhook.room", &doorhook.room)?;
104 validate_name_token("doorhook.door", &doorhook.door)?;
105 write_cp1252(
106 writer,
107 &format!(
108 "{LYT_INDENT}{LYT_INDENT}{} {} 0 {} {} {} {} {} {} {}{LYT_LINE_SEP}",
109 doorhook.room,
110 doorhook.door,
111 fmt_f32(doorhook.position.x),
112 fmt_f32(doorhook.position.y),
113 fmt_f32(doorhook.position.z),
114 fmt_f32(doorhook.orientation.x),
115 fmt_f32(doorhook.orientation.y),
116 fmt_f32(doorhook.orientation.z),
117 fmt_f32(doorhook.orientation.w)
118 ),
119 format!("doorhook `{}`/`{}`", doorhook.room, doorhook.door),
120 )?;
121 }
122
123 write_cp1252(writer, "donelayout", "footer".into())?;
124 Ok(())
125}
126
127#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(lyt)))]
134pub fn write_lyt_to_vec(lyt: &Lyt) -> Result<Vec<u8>, LytError> {
135 let mut cursor = Cursor::new(Vec::new());
136 write_lyt(&mut cursor, lyt)?;
137 Ok(cursor.into_inner())
138}
139
140fn validate_name_token(field: &'static str, value: &str) -> Result<(), LytError> {
141 if value.is_empty() || value.chars().any(char::is_whitespace) {
142 return Err(LytError::InvalidName {
143 field,
144 value: value.to_string(),
145 });
146 }
147 Ok(())
148}
149
150fn fmt_f32(v: f32) -> String {
161 let s = format!("{v}");
162 if s.contains('.') || s.contains('e') || s.contains('E') {
163 s
164 } else {
165 format!("{s}.0")
166 }
167}
168
169fn write_cp1252<W: Write>(writer: &mut W, text: &str, context: String) -> Result<(), LytError> {
170 binary::write_cp1252(writer, text, context, |context, source| {
171 LytError::TextEncoding { context, source }
172 })
173}