Skip to main content

rakata_formats/txi/
mod.rs

1//! TXI ASCII reader and writer.
2//!
3//! TXI (texture info) resources are line-based text sidecars for texture
4//! resources. Each directive is a command followed by optional arguments.
5//!
6//! ## Shape of the file
7//!
8//! One directive per line, a command token followed by its arguments. No
9//! header, no terminator, no required order. The only structure beyond a flat
10//! list is `upperleftcoords` and `lowerrightcoords`, which declare a count and
11//! are followed by that many coordinate triples.
12//!
13//! Empty lines are ignored and command matching is case-insensitive. Two write
14//! modes exist because both are legitimate: policy-normalised regularises
15//! command spelling, source-preserving keeps whatever the input used so someone
16//! else's file round-trips without being editorialised.
17//!
18//! Text is decoded and encoded as Windows-1252. The grammar and the engine's
19//! parse behaviour, including how unrecognised directives and malformed boolean
20//! arguments fail silently, are in `docs/src/formats/textures/txi.md`.
21
22mod reader;
23mod writer;
24
25pub use reader::{
26    read_txi, read_txi_from_bytes, read_txi_from_bytes_with_options, read_txi_with_options,
27};
28pub use writer::{
29    write_txi, write_txi_to_vec, write_txi_to_vec_with_options, write_txi_with_options,
30};
31
32use thiserror::Error;
33
34use rakata_core::{DecodeTextError, EncodeTextError};
35
36use crate::binary::{DecodeBinary, EncodeBinary};
37
38const UPPER_LEFT_COORDS_COMMAND: &str = "upperleftcoords";
39const LOWER_RIGHT_COORDS_COMMAND: &str = "lowerrightcoords";
40const DECAL1_ALIAS: &str = "decal1";
41const DECAL_COMMAND: &str = "decal";
42
43/// Reader options for TXI parsing.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
45pub struct TxiReadOptions {
46    /// Enables compatibility aliasing of `decal1` to `decal 1`.
47    ///
48    /// Native K1 TXI parse paths match `decal` but do not expose a `decal1`
49    /// token in the observed string table. Keep this disabled by default.
50    pub compatibility_decal1_alias: bool,
51}
52
53/// Writer options for TXI serialization.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
55pub struct TxiWriteOptions {}
56
57/// One TXI UV coordinate entry.
58#[derive(Debug, Clone, PartialEq)]
59pub struct TxiCoordinate {
60    /// First coordinate component.
61    pub u: f32,
62    /// Second coordinate component.
63    pub v: f32,
64    /// Third coordinate component (often 0).
65    pub w: i32,
66}
67
68/// One TXI command with optional argument payload.
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct TxiDirective {
71    /// Command token.
72    ///
73    /// In policy-normalized mode this is lowercase-normalized.
74    pub command: String,
75    /// Raw argument text after the first whitespace separator.
76    pub arguments: String,
77}
78
79/// One TXI coordinate block command.
80#[derive(Debug, Clone, PartialEq)]
81pub struct TxiCoordinateBlock {
82    /// Command token (`upperleftcoords` or `lowerrightcoords` in
83    /// policy-normalized mode).
84    pub command: String,
85    /// Count declared on the command line.
86    pub declared_count: usize,
87    /// Parsed coordinate rows.
88    pub coordinates: Vec<TxiCoordinate>,
89}
90
91/// One ordered TXI entry.
92#[derive(Debug, Clone, PartialEq)]
93pub enum TxiEntry {
94    /// A plain command line.
95    Directive(TxiDirective),
96    /// A coordinate block command plus zero or more parsed rows.
97    CoordinateBlock(TxiCoordinateBlock),
98}
99
100/// In-memory TXI container.
101#[derive(Debug, Clone, PartialEq, Default)]
102pub struct Txi {
103    /// Ordered entries as parsed or appended.
104    pub entries: Vec<TxiEntry>,
105}
106
107impl Txi {
108    /// Creates an empty TXI container.
109    pub fn new() -> Self {
110        Self::default()
111    }
112
113    /// Appends a plain command entry.
114    pub fn push_directive(
115        &mut self,
116        command: impl Into<String>,
117        arguments: impl Into<String>,
118    ) -> &mut Self {
119        self.entries.push(TxiEntry::Directive(TxiDirective {
120            command: command.into().to_ascii_lowercase(),
121            arguments: arguments.into(),
122        }));
123        self
124    }
125
126    /// Appends a coordinate block entry.
127    pub fn push_coordinate_block(
128        &mut self,
129        command: impl Into<String>,
130        declared_count: usize,
131        coordinates: Vec<TxiCoordinate>,
132    ) -> &mut Self {
133        self.entries
134            .push(TxiEntry::CoordinateBlock(TxiCoordinateBlock {
135                command: command.into().to_ascii_lowercase(),
136                declared_count,
137                coordinates,
138            }));
139        self
140    }
141}
142
143impl DecodeBinary for Txi {
144    type Error = TxiError;
145
146    fn decode_binary(bytes: &[u8]) -> Result<Self, Self::Error> {
147        read_txi_from_bytes(bytes)
148    }
149}
150
151impl EncodeBinary for Txi {
152    type Error = TxiError;
153
154    fn encode_binary(&self) -> Result<Vec<u8>, Self::Error> {
155        write_txi_to_vec(self)
156    }
157}
158
159/// Errors produced while parsing or serializing TXI text data.
160#[derive(Debug, Error)]
161pub enum TxiError {
162    /// I/O read/write failure.
163    #[error(transparent)]
164    Io(#[from] std::io::Error),
165    /// TXI text is structurally invalid.
166    #[error("invalid TXI data: {0}")]
167    InvalidData(String),
168    /// Text cannot be represented in Windows-1252 output.
169    #[error("TXI text encoding failed for {context}: {source}")]
170    TextEncoding {
171        /// Value context.
172        context: String,
173        /// Encoding error details.
174        #[source]
175        source: EncodeTextError,
176    },
177    /// Input bytes could not be decoded losslessly as Windows-1252.
178    #[error("TXI text decoding failed for {context}: {source}")]
179    TextDecoding {
180        /// Value context.
181        context: String,
182        /// Decoding error details.
183        #[source]
184        source: DecodeTextError,
185    },
186}