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