1mod reader;
30mod writer;
31
32pub use reader::{read_tpc, read_tpc_from_bytes};
33pub use writer::{write_tpc, write_tpc_to_vec};
34
35use std::io::Write;
36use thiserror::Error;
37
38use rakata_core::{encode_text, DecodeTextError, EncodeTextError, TextEncoding};
39
40use crate::binary::{self, write_f32, write_u8, DecodeBinary, EncodeBinary};
41
42const FILE_HEADER_SIZE: usize = 128;
43const RESERVED_SIZE: usize = 114;
44const CUBEMAP_LAYER_COUNT: usize = 6;
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
48pub enum TpcHeaderPixelFormat {
49 Greyscale,
51 Rgb,
53 Rgba,
55 Dxt1,
57 Dxt5,
65}
66
67impl TpcHeaderPixelFormat {
68 fn is_compressed(self) -> bool {
69 matches!(self, Self::Dxt1 | Self::Dxt5)
70 }
71
72 fn bytes_per_pixel(self) -> Option<usize> {
73 match self {
74 Self::Greyscale => Some(1),
75 Self::Rgb => Some(3),
76 Self::Rgba => Some(4),
77 Self::Dxt1 | Self::Dxt5 => None,
78 }
79 }
80
81 fn bytes_per_block(self) -> Option<usize> {
82 match self {
83 Self::Dxt1 => Some(8),
84 Self::Dxt5 => Some(16),
85 _ => None,
86 }
87 }
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92pub struct TpcPixelFormatCode {
93 pub compressed: bool,
95 pub pixel_type: u8,
97}
98
99impl TpcPixelFormatCode {
100 pub const fn known_format(self) -> Option<TpcHeaderPixelFormat> {
102 match (self.compressed, self.pixel_type) {
103 (false, 1) => Some(TpcHeaderPixelFormat::Greyscale),
104 (false, 2) => Some(TpcHeaderPixelFormat::Rgb),
105 (false, 4) => Some(TpcHeaderPixelFormat::Rgba),
106 (true, 2) => Some(TpcHeaderPixelFormat::Dxt1),
107 (true, 4) => Some(TpcHeaderPixelFormat::Dxt5),
108 _ => None,
109 }
110 }
111}
112
113#[derive(Debug, Clone, PartialEq)]
115pub struct TpcHeader {
116 pub data_size: u32,
120 pub alpha_test: f32,
122 pub width: u16,
124 pub height: u16,
126 pub pixel_type: u8,
128 pub mipmap_count: u8,
130 pub reserved: [u8; RESERVED_SIZE],
132}
133
134impl TpcHeader {
135 pub const fn compressed(&self) -> bool {
137 self.data_size != 0
138 }
139
140 pub const fn pixel_format_code(&self) -> TpcPixelFormatCode {
142 TpcPixelFormatCode {
143 compressed: self.compressed(),
144 pixel_type: self.pixel_type,
145 }
146 }
147}
148
149#[derive(Debug, Clone, PartialEq)]
151pub struct Tpc {
152 pub header: TpcHeader,
154 pub payload: Vec<u8>,
156 pub txi_footer: Vec<u8>,
158}
159
160impl Tpc {
161 pub fn new(header: TpcHeader, payload: Vec<u8>, txi_footer: Vec<u8>) -> Self {
163 Self {
164 header,
165 payload,
166 txi_footer,
167 }
168 }
169
170 pub fn known_pixel_format(&self) -> Option<TpcHeaderPixelFormat> {
172 self.header.pixel_format_code().known_format()
173 }
174
175 pub fn is_cube_map(&self) -> bool {
177 let width = usize::from(self.header.width);
178 let height = usize::from(self.header.height);
179 self.header.compressed()
180 && width > 0
181 && height % CUBEMAP_LAYER_COUNT == 0
182 && (height / CUBEMAP_LAYER_COUNT == width)
183 }
184
185 pub fn txi_text(&self) -> String {
187 rakata_core::decode_text(&self.txi_footer, TextEncoding::Windows1252)
188 }
189
190 pub fn txi_text_strict(&self) -> Result<String, DecodeTextError> {
198 rakata_core::decode_text_strict(&self.txi_footer, TextEncoding::Windows1252)
199 }
200
201 pub fn set_txi_text(&mut self, text: &str) -> Result<(), EncodeTextError> {
208 self.txi_footer = encode_text(text, TextEncoding::Windows1252)?;
209 Ok(())
210 }
211}
212
213impl DecodeBinary for Tpc {
214 type Error = TpcBinaryError;
215
216 fn decode_binary(bytes: &[u8]) -> Result<Self, Self::Error> {
217 read_tpc_from_bytes(bytes)
218 }
219}
220
221impl EncodeBinary for Tpc {
222 type Error = TpcBinaryError;
223
224 fn encode_binary(&self) -> Result<Vec<u8>, Self::Error> {
225 write_tpc_to_vec(self)
226 }
227}
228
229#[derive(Debug, Error)]
231pub enum TpcBinaryError {
232 #[error(transparent)]
234 Io(#[from] std::io::Error),
235 #[error("invalid TPC header: {0}")]
237 InvalidHeader(String),
238 #[error("invalid TPC data: {0}")]
240 InvalidData(String),
241 #[error(
243 "unsupported TPC pixel type: compressed={}, pixel_type={}",
244 .0.compressed,
245 .0.pixel_type
246 )]
247 UnsupportedPixelType(TpcPixelFormatCode),
248 #[error("value overflow while handling field `{0}`")]
250 ValueOverflow(&'static str),
251}
252
253impl From<binary::BinaryLayoutError> for TpcBinaryError {
254 fn from(error: binary::BinaryLayoutError) -> Self {
255 Self::InvalidHeader(error.to_string())
256 }
257}
258
259fn read_header(bytes: &[u8]) -> Result<TpcHeader, TpcBinaryError> {
260 let data_size = binary::read_u32(bytes, 0)?;
261 let alpha_test = binary::read_f32(bytes, 4)?;
262 let width = binary::read_u16(bytes, 8)?;
263 let height = binary::read_u16(bytes, 10)?;
264 let pixel_type = bytes[12];
265 let mipmap_count = bytes[13];
266 let mut reserved = [0_u8; RESERVED_SIZE];
267 reserved.copy_from_slice(&bytes[14..FILE_HEADER_SIZE]);
268
269 Ok(TpcHeader {
270 data_size,
271 alpha_test,
272 width,
273 height,
274 pixel_type,
275 mipmap_count,
276 reserved,
277 })
278}
279
280fn write_header<W: Write>(writer: &mut W, header: &TpcHeader) -> Result<(), TpcBinaryError> {
281 binary::write_u32(writer, header.data_size)?;
282 write_f32(writer, header.alpha_test)?;
283 binary::write_u16(writer, header.width)?;
284 binary::write_u16(writer, header.height)?;
285 write_u8(writer, header.pixel_type)?;
286 write_u8(writer, header.mipmap_count)?;
287 writer.write_all(&header.reserved)?;
288 Ok(())
289}
290
291fn expected_payload_size(header: &TpcHeader) -> Result<usize, TpcBinaryError> {
292 let width = usize::from(header.width);
293 let mut height = usize::from(header.height);
294 if width == 0 || height == 0 {
295 return Err(TpcBinaryError::InvalidHeader(
296 "width/height must be non-zero".into(),
297 ));
298 }
299
300 let mip_levels = usize::from(header.mipmap_count);
301 if mip_levels == 0 {
302 return Err(TpcBinaryError::InvalidHeader(
303 "mipmap_count must be at least 1".into(),
304 ));
305 }
306
307 let code = header.pixel_format_code();
308 let format = code
309 .known_format()
310 .ok_or(TpcBinaryError::UnsupportedPixelType(code))?;
311
312 let layer_count = if format.is_compressed()
313 && height % CUBEMAP_LAYER_COUNT == 0
314 && (height / CUBEMAP_LAYER_COUNT == width)
315 {
316 height /= CUBEMAP_LAYER_COUNT;
317 CUBEMAP_LAYER_COUNT
318 } else {
319 1
320 };
321
322 let base_level_size = if format.is_compressed() {
323 binary::checked_to_usize(header.data_size, "data_size")
324 .map_err(|_| TpcBinaryError::ValueOverflow("data_size"))?
325 } else {
326 let bpp = format
327 .bytes_per_pixel()
328 .ok_or_else(|| TpcBinaryError::InvalidHeader("unexpected compressed format".into()))?;
329 checked_uncompressed_level_size(width, height, bpp)?
330 };
331
332 let mut per_layer_size = base_level_size;
333 let mut level_width = width;
334 let mut level_height = height;
335 for _ in 1..mip_levels {
336 level_width >>= 1;
338 level_height >>= 1;
339 per_layer_size = per_layer_size
340 .checked_add(mip_level_size(format, level_width, level_height)?)
341 .ok_or(TpcBinaryError::ValueOverflow("mip level sum"))?;
342 }
343
344 per_layer_size
345 .checked_mul(layer_count)
346 .ok_or(TpcBinaryError::ValueOverflow("layer size sum"))
347}
348
349fn mip_level_size(
350 format: TpcHeaderPixelFormat,
351 width: usize,
352 height: usize,
353) -> Result<usize, TpcBinaryError> {
354 if let Some(bytes_per_pixel) = format.bytes_per_pixel() {
355 return checked_uncompressed_level_size(width, height, bytes_per_pixel);
356 }
357
358 let block_size = format
359 .bytes_per_block()
360 .ok_or_else(|| TpcBinaryError::InvalidHeader("missing block size".into()))?;
361 let block_width = width.div_ceil(4);
362 let block_height = height.div_ceil(4);
363 let block_count = block_width
364 .checked_mul(block_height)
365 .ok_or(TpcBinaryError::ValueOverflow("block count"))?;
366 block_count
367 .checked_mul(block_size)
368 .ok_or(TpcBinaryError::ValueOverflow("block bytes"))
369}
370
371fn checked_uncompressed_level_size(
372 width: usize,
373 height: usize,
374 bytes_per_pixel: usize,
375) -> Result<usize, TpcBinaryError> {
376 width
377 .checked_mul(height)
378 .and_then(|pixels| pixels.checked_mul(bytes_per_pixel))
379 .ok_or(TpcBinaryError::ValueOverflow("uncompressed level size"))
380}