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}