rakata_formats/txi/
writer.rs

1//! TXI ASCII writer.
2
3use std::io::{Cursor, Write};
4
5use crate::binary;
6
7use super::{Txi, TxiEntry, TxiError, TxiWriteOptions};
8
9/// Writes TXI data to a writer.
10#[cfg_attr(
11    feature = "tracing",
12    tracing::instrument(level = "debug", skip(writer, txi))
13)]
14pub fn write_txi<W: Write>(writer: &mut W, txi: &Txi) -> Result<(), TxiError> {
15    write_txi_with_options(writer, txi, TxiWriteOptions::default())
16}
17
18/// Writes TXI data to a writer with explicit serialization options.
19#[cfg_attr(
20    feature = "tracing",
21    tracing::instrument(level = "debug", skip(writer, txi, _options))
22)]
23pub fn write_txi_with_options<W: Write>(
24    writer: &mut W,
25    txi: &Txi,
26    _options: TxiWriteOptions,
27) -> Result<(), TxiError> {
28    let mut lines = Vec::new();
29
30    for entry in &txi.entries {
31        match entry {
32            TxiEntry::Directive(directive) => {
33                if directive.arguments.is_empty() {
34                    lines.push(directive.command.clone());
35                } else {
36                    lines.push(format!("{} {}", directive.command, directive.arguments));
37                }
38            }
39            TxiEntry::CoordinateBlock(block) => {
40                let command = &block.command;
41                let declared_count = block.declared_count;
42                lines.push(format!("{command} {declared_count}"));
43                for coordinate in &block.coordinates {
44                    lines.push(format!(
45                        "{} {} {}",
46                        coordinate.u, coordinate.v, coordinate.w
47                    ));
48                }
49            }
50        }
51    }
52
53    // Each line is LF-terminated (not just LF-separated) to match vanilla TXI output.
54    //
55    // Ghidra evidence (`CAurTextureBasic::Init` `0x00422af0`, `getnextline_res` `0x0044be70`,
56    // `getnextline_file` `0x0044be20`): both resource and file paths scan for `'\n'` as the
57    // line boundary. `firstword` (`0x00463530`) also terminates on `'\r'`, so CRLF files are
58    // tolerated, but LF is the canonical choice here (vanilla TXI files use LF).
59    let text = lines.iter().map(|l| format!("{l}\n")).collect::<String>();
60    binary::write_cp1252(writer, &text, "TXI payload".into(), |context, source| {
61        TxiError::TextEncoding { context, source }
62    })
63}
64
65/// Serializes TXI data to bytes.
66#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(txi)))]
67pub fn write_txi_to_vec(txi: &Txi) -> Result<Vec<u8>, TxiError> {
68    write_txi_to_vec_with_options(txi, TxiWriteOptions::default())
69}
70
71/// Serializes TXI data to bytes with explicit serialization options.
72#[cfg_attr(
73    feature = "tracing",
74    tracing::instrument(level = "debug", skip(txi, options))
75)]
76pub fn write_txi_to_vec_with_options(
77    txi: &Txi,
78    options: TxiWriteOptions,
79) -> Result<Vec<u8>, TxiError> {
80    let mut cursor = Cursor::new(Vec::new());
81    write_txi_with_options(&mut cursor, txi, options)?;
82    Ok(cursor.into_inner())
83}