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#[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/// Serializes a LYT layout to bytes.
121#[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
138/// Formats an f32 for LYT output, ensuring at least one decimal place.
139///
140/// Rust's `Display` for f32 uses the shortest round-trip representation, which
141/// drops the decimal for whole numbers (`0.0` -> `"0"`).
142///
143/// Ghidra evidence (`CLYT::LoadLayout` `0x005de900`): the engine parses floats with
144/// plain `%f` (C sscanf), which accepts any valid decimal representation including
145/// `"0"` and `"100"`. The `.0` suffix is therefore **not required for engine
146/// compatibility**. We append it anyway to match vanilla Aurora Toolset output
147/// conventions (`"0.0"`, `"100.0"`, `"3688.0"` observed in game LYT files).
148fn 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}