Skip to main content

rakata_formats/lyt/
writer.rs

1//! LYT ASCII writer.
2
3use 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/// Writes a LYT layout to a writer.
13///
14/// # Errors
15///
16/// [`LytError::InvalidName`] when a room or track name holds whitespace,
17/// which the whitespace-separated grammar cannot represent,
18/// [`LytError::TextEncoding`] when a name has no Windows-1252 form, and
19/// [`LytError::Io`] when the writer fails.
20#[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/// Serializes a LYT layout to bytes.
128///
129/// # Errors
130///
131/// [`LytError::InvalidName`] and [`LytError::TextEncoding`] on the terms
132/// [`write_lyt`] gives. The `Vec` target has no I/O to fail at.
133#[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
150/// Formats an f32 for LYT output, ensuring at least one decimal place.
151///
152/// Rust's `Display` for f32 uses the shortest round-trip representation, which
153/// drops the decimal for whole numbers (`0.0` -> `"0"`).
154///
155/// Ghidra evidence (`CLYT::LoadLayout` `0x005de900`): the engine parses floats with
156/// plain `%f` (C sscanf), which accepts any valid decimal representation including
157/// `"0"` and `"100"`. The `.0` suffix is therefore **not required for engine
158/// compatibility**. It is appended anyway to match vanilla Aurora Toolset
159/// output (`"0.0"`, `"100.0"`, `"3688.0"` observed in game LYT files).
160fn 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}