Skip to main content

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///
11/// # Errors
12///
13/// The same as [`write_txi_with_options`] under the default options.
14#[cfg_attr(
15    feature = "tracing",
16    tracing::instrument(level = "debug", skip(writer, txi))
17)]
18pub fn write_txi<W: Write>(writer: &mut W, txi: &Txi) -> Result<(), TxiError> {
19    write_txi_with_options(writer, txi, TxiWriteOptions::default())
20}
21
22/// Writes TXI data to a writer with explicit serialization options.
23///
24/// # Errors
25///
26/// [`TxiError::TextEncoding`] when a command or its arguments have no form in
27/// the target encoding, and [`TxiError::Io`] when the writer fails. TXI is
28/// plain text with no widths to overflow, so those are the only two.
29#[cfg_attr(
30    feature = "tracing",
31    tracing::instrument(level = "debug", skip(writer, txi, _options))
32)]
33pub fn write_txi_with_options<W: Write>(
34    writer: &mut W,
35    txi: &Txi,
36    _options: TxiWriteOptions,
37) -> Result<(), TxiError> {
38    let mut lines = Vec::new();
39
40    for entry in &txi.entries {
41        match entry {
42            TxiEntry::Directive(directive) => {
43                if directive.arguments.is_empty() {
44                    lines.push(directive.command.clone());
45                } else {
46                    lines.push(format!("{} {}", directive.command, directive.arguments));
47                }
48            }
49            TxiEntry::CoordinateBlock(block) => {
50                let command = &block.command;
51                let declared_count = block.declared_count;
52                lines.push(format!("{command} {declared_count}"));
53                for coordinate in &block.coordinates {
54                    lines.push(format!(
55                        "{} {} {}",
56                        coordinate.u, coordinate.v, coordinate.w
57                    ));
58                }
59            }
60        }
61    }
62
63    // Each line is LF-terminated (not just LF-separated) to match vanilla TXI output.
64    //
65    // Ghidra evidence (`CAurTextureBasic::Init` `0x00422af0`, `getnextline_res` `0x0044be70`,
66    // `getnextline_file` `0x0044be20`): both resource and file paths scan for `'\n'` as the
67    // line boundary. `firstword` (`0x00463530`) also terminates on `'\r'`, so CRLF files are
68    // tolerated, but LF is the canonical choice here (vanilla TXI files use LF).
69    let text = lines.iter().map(|l| format!("{l}\n")).collect::<String>();
70    binary::write_cp1252(writer, &text, "TXI payload".into(), |context, source| {
71        TxiError::TextEncoding { context, source }
72    })
73}
74
75/// Serializes TXI data to bytes.
76///
77/// # Errors
78///
79/// The same as [`write_txi_to_vec_with_options`] under the default options.
80#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(txi)))]
81pub fn write_txi_to_vec(txi: &Txi) -> Result<Vec<u8>, TxiError> {
82    write_txi_to_vec_with_options(txi, TxiWriteOptions::default())
83}
84
85/// Serializes TXI data to bytes with explicit serialization options.
86///
87/// # Errors
88///
89/// [`TxiError::TextEncoding`] on the terms [`write_txi_with_options`] gives.
90/// The `Vec` target has no I/O to fail at.
91#[cfg_attr(
92    feature = "tracing",
93    tracing::instrument(level = "debug", skip(txi, options))
94)]
95pub fn write_txi_to_vec_with_options(
96    txi: &Txi,
97    options: TxiWriteOptions,
98) -> Result<Vec<u8>, TxiError> {
99    let mut cursor = Cursor::new(Vec::new());
100    write_txi_with_options(&mut cursor, txi, options)?;
101    Ok(cursor.into_inner())
102}