1use crate::binary::{Error, Reader, Result, Writer};
4use crate::records::{hex, unhex};
5use serde::{Deserialize, Serialize};
6
7pub const DXT1: u32 = 0x31545844;
9pub const DXT3: u32 = 0x33545844;
11pub const DXT5: u32 = 0x35545844;
13
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16pub struct TextureLevel {
17 pub width: u32,
19 pub height: u32,
21 pub format: u32,
23 pub data: String,
25}
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28#[serde(rename_all = "camelCase")]
29pub struct Texture {
30 pub name: String,
32 pub version: u8,
34 pub width: u32,
36 pub height: u32,
38 pub format: u32,
40 pub mips: u32,
42 pub levels: Vec<TextureLevel>,
44 pub fallback_levels: Vec<TextureLevel>,
46 pub trailing: String,
48}
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct TerrainColors {
53 pub grid_size: usize,
55 pub textures: Vec<Texture>,
57}
58fn error(message: &str) -> Error {
59 Error {
60 offset: 0,
61 message: message.into(),
62 }
63}
64fn compressed(format: u32) -> bool {
65 matches!(format, DXT1 | DXT3 | DXT5)
66}
67fn bc(format: u32) -> Result<texpresso::Format> {
68 match format {
69 DXT1 => Ok(texpresso::Format::Bc1),
70 DXT3 => Ok(texpresso::Format::Bc2),
71 DXT5 => Ok(texpresso::Format::Bc3),
72 _ => Err(error("Unsupported block format")),
73 }
74}
75pub fn mip_size(format: u32, width: u32, height: u32) -> Result<usize> {
77 if width == 0 || height == 0 || width > 16384 || height > 16384 {
78 return Err(error("Invalid texture dimensions"));
79 }
80 let size = if compressed(format) {
81 width.div_ceil(4) as usize
82 * height.div_ceil(4) as usize
83 * if format == DXT1 { 8 } else { 16 }
84 } else {
85 width as usize
86 * height as usize
87 * match format {
88 0 | 23..=26 => 2,
89 20 => 3,
90 21 | 22 => 4,
91 _ => return Err(error("Unsupported texture format")),
92 }
93 };
94 if size > 256 * 1024 * 1024 {
95 return Err(error("Texture byte budget exceeded"));
96 }
97 Ok(size)
98}
99fn plan(
100 format: u32,
101 width: u32,
102 height: u32,
103 mips: u32,
104) -> Result<(Vec<TextureLevel>, Vec<TextureLevel>)> {
105 mip_size(format, width, height)?;
106 if mips > 1 {
107 return Err(error("Invalid mip flag"));
108 }
109 let level = |wide, high, kind| TextureLevel {
110 width: wide,
111 height: high,
112 format: kind,
113 data: String::new(),
114 };
115 let mut primary = vec![level(width, height, format)];
116 let mut fallback = Vec::new();
117 if mips == 1 {
118 let (mut wide, mut high) = (width / 2, height / 2);
119 while wide >= 4 && high >= 4 {
120 primary.push(level(wide, high, format));
121 wide /= 2;
122 high /= 2;
123 }
124 }
125 if compressed(format) {
126 let kind = if format == DXT1 { 25 } else { 26 };
127 if mips == 1 {
128 let (mut wide, mut high) = (width / 2, height / 2);
129 while wide >= 4 && high >= 4 {
130 fallback.push(level(wide, high, kind));
131 wide /= 2;
132 high /= 2;
133 }
134 } else {
135 fallback.push(level((width / 2).max(1), (height / 2).max(1), kind));
136 if width >= 1024 {
137 fallback.push(level(256, 256, kind));
138 }
139 }
140 } else if mips == 0 && width >= 512 {
141 fallback.push(level(256, 256, 0));
142 }
143 Ok((primary, fallback))
144}
145pub fn read_texture(reader: &mut Reader<'_>) -> Result<Texture> {
147 let name = hex(&reader.string(512)?);
148 if reader.raw(3)? != b"NTF" {
149 return reader.fail("Missing NTF signature");
150 }
151 let version = reader.u8()?;
152 let width = reader.u32()?;
153 let height = reader.u32()?;
154 let format = reader.u32()?;
155 let mips = reader.u32()?;
156 let (mut levels, mut fallback_levels) = plan(format, width, height, mips)?;
157 for level in levels.iter_mut().chain(fallback_levels.iter_mut()) {
158 level.data = hex(reader.raw(mip_size(level.format, level.width, level.height)?)?);
159 }
160 Ok(Texture {
161 name,
162 version,
163 width,
164 height,
165 format,
166 mips,
167 levels,
168 fallback_levels,
169 trailing: String::new(),
170 })
171}
172pub fn parse_texture(bytes: &[u8]) -> Result<Texture> {
174 if bytes.len() > 256 * 1024 * 1024 {
175 return Err(error("Texture input budget exceeded"));
176 }
177 let mut reader = Reader::new(bytes);
178 let mut texture = read_texture(&mut reader)?;
179 texture.trailing = hex(reader.raw(reader.remaining())?);
180 Ok(texture)
181}
182pub fn append_texture(writer: &mut Writer, texture: &Texture) -> Result<()> {
184 let (primary, fallback) = plan(texture.format, texture.width, texture.height, texture.mips)?;
185 let name = unhex(&texture.name)?;
186 if name.len() > 512 {
187 return Err(error("Texture name too long"));
188 }
189 writer.string(&name)?;
190 writer.raw(b"NTF")?;
191 writer.u8(texture.version)?;
192 writer.u32(texture.width)?;
193 writer.u32(texture.height)?;
194 writer.u32(texture.format)?;
195 writer.u32(texture.mips)?;
196 for (expected, supplied) in [
197 (&primary, &texture.levels),
198 (&fallback, &texture.fallback_levels),
199 ] {
200 if expected.len() != supplied.len() {
201 return Err(error("Texture mip count mismatch"));
202 }
203 for (level, stored) in expected.iter().zip(supplied) {
204 let bytes = unhex(&stored.data)?;
205 if level.width != stored.width
206 || level.height != stored.height
207 || level.format != stored.format
208 || bytes.len() != mip_size(level.format, level.width, level.height)?
209 {
210 return Err(error("Texture level layout mismatch"));
211 }
212 writer.raw(&bytes)?;
213 }
214 }
215 writer.raw(&unhex(&texture.trailing)?)
216}
217pub fn write_texture(texture: &Texture) -> Result<Vec<u8>> {
219 let mut writer = Writer::default();
220 append_texture(&mut writer, texture)?;
221 Ok(writer.finish())
222}
223pub fn parse_gtt(bytes: &[u8]) -> Result<Vec<Texture>> {
225 let mut reader = Reader::new(bytes);
226 let mut textures = Vec::new();
227 while reader.remaining() > 0 {
228 if textures.len() >= 65536 {
229 return Err(error("Texture collection limit exceeded"));
230 }
231 textures.push(read_texture(&mut reader)?);
232 }
233 if textures.is_empty() {
234 return Err(error("Empty texture collection"));
235 }
236 Ok(textures)
237}
238pub fn write_gtt(textures: &[Texture]) -> Result<Vec<u8>> {
240 if textures.is_empty() || textures.len() > 65536 {
241 return Err(error("Invalid texture collection length"));
242 }
243 let mut writer = Writer::default();
244 for texture in textures {
245 if !texture.trailing.is_empty() {
246 return Err(error("Embedded texture cannot carry a suffix"));
247 }
248 append_texture(&mut writer, texture)?;
249 }
250 Ok(writer.finish())
251}
252pub fn parse_tct(bytes: &[u8]) -> Result<TerrainColors> {
254 let textures = parse_gtt(bytes)?;
255 let grid_size = (textures.len() as f64).sqrt() as usize;
256 if grid_size * grid_size != textures.len() {
257 return Err(error("TCT texture count must be square"));
258 }
259 Ok(TerrainColors {
260 grid_size,
261 textures,
262 })
263}
264pub fn write_tct(colors: &TerrainColors) -> Result<Vec<u8>> {
266 if colors.grid_size == 0
267 || colors.grid_size > 256
268 || colors.grid_size * colors.grid_size != colors.textures.len()
269 {
270 return Err(error("TCT grid mismatch"));
271 }
272 write_gtt(&colors.textures)
273}
274fn expand(value: u16, bits: u32) -> u8 {
275 ((u32::from(value) * 255 + ((1 << bits) - 1) / 2) / ((1 << bits) - 1)) as u8
276}
277pub fn decode_level(level: &TextureLevel) -> Result<Vec<u8>> {
279 let size = mip_size(level.format, level.width, level.height)?;
280 let bytes = unhex(&level.data)?;
281 if bytes.len() != size
282 || level.width as usize * level.height as usize > 16_777_216
283 || level.format == 0
284 {
285 return Err(error(
286 "Invalid payload, legacy pixels or decoded pixel budget",
287 ));
288 }
289 let mut rgba = vec![0; level.width as usize * level.height as usize * 4];
290 if compressed(level.format) {
291 bc(level.format)?.decompress(
292 &bytes,
293 level.width as usize,
294 level.height as usize,
295 &mut rgba,
296 );
297 return Ok(rgba);
298 }
299 let mut reader = Reader::new(&bytes);
300 for pixel in rgba.as_chunks_mut::<4>().0 {
301 if level.format <= 22 {
302 let blue = reader.u8()?;
303 let green = reader.u8()?;
304 let red = reader.u8()?;
305 let alpha = if level.format == 21 {
306 reader.u8()?
307 } else {
308 if level.format == 22 {
309 reader.u8()?;
310 }
311 255
312 };
313 pixel.copy_from_slice(&[red, green, blue, alpha]);
314 } else {
315 let word = reader.u16()?;
316 let color = match level.format {
317 23 => [
318 expand(word >> 11, 5),
319 expand((word >> 5) & 63, 6),
320 expand(word & 31, 5),
321 255,
322 ],
323 26 => [
324 expand((word >> 8) & 15, 4),
325 expand((word >> 4) & 15, 4),
326 expand(word & 15, 4),
327 expand(word >> 12, 4),
328 ],
329 _ => [
330 expand((word >> 10) & 31, 5),
331 expand((word >> 5) & 31, 5),
332 expand(word & 31, 5),
333 if level.format == 25 && word & 0x8000 == 0 {
334 0
335 } else {
336 255
337 },
338 ],
339 };
340 pixel.copy_from_slice(&color);
341 }
342 }
343 Ok(rgba)
344}
345pub fn encode_level(format: u32, width: u32, height: u32, rgba: &[u8]) -> Result<TextureLevel> {
347 let size = mip_size(format, width, height)?;
348 if width as usize * height as usize > 16_777_216
349 || rgba.len() != width as usize * height as usize * 4
350 || format == 0
351 {
352 return Err(error("RGBA dimensions or pixel budget mismatch"));
353 }
354 let bytes = if compressed(format) {
355 let mut bytes = vec![0; size];
356 bc(format)?.compress(
357 rgba,
358 width as usize,
359 height as usize,
360 texpresso::Params::default(),
361 &mut bytes,
362 );
363 bytes
364 } else {
365 let mut writer = Writer::default();
366 let quantize = |value: u8, bits: u32| -> u16 {
367 ((u32::from(value) * ((1 << bits) - 1) + 127) / 255) as u16
368 };
369 for pixel in rgba.as_chunks::<4>().0 {
370 let (red, green, blue, alpha) = (pixel[0], pixel[1], pixel[2], pixel[3]);
371 if format <= 22 {
372 writer.raw(&[blue, green, red])?;
373 if format != 20 {
374 writer.u8(if format == 21 { alpha } else { 255 })?;
375 }
376 } else {
377 let word = match format {
378 23 => (quantize(red, 5) << 11) | (quantize(green, 6) << 5) | quantize(blue, 5),
379 26 => {
380 (quantize(alpha, 4) << 12)
381 | (quantize(red, 4) << 8)
382 | (quantize(green, 4) << 4)
383 | quantize(blue, 4)
384 }
385 _ => {
386 (u16::from(format == 24 || alpha >= 128) << 15)
387 | (quantize(red, 5) << 10)
388 | (quantize(green, 5) << 5)
389 | quantize(blue, 5)
390 }
391 };
392 writer.u16(word)?;
393 }
394 }
395 writer.finish()
396 };
397 Ok(TextureLevel {
398 width,
399 height,
400 format,
401 data: hex(&bytes),
402 })
403}
404pub fn create_texture(
406 width: u32,
407 height: u32,
408 rgba: &[u8],
409 format: u32,
410 mips: bool,
411) -> Result<Texture> {
412 let (mut levels, mut fallback_levels) = plan(format, width, height, u32::from(mips))?;
413 if width as usize * height as usize > 16_777_216
414 || rgba.len() != width as usize * height as usize * 4
415 {
416 return Err(error("RGBA byte count mismatch"));
417 }
418 for level in levels.iter_mut().chain(fallback_levels.iter_mut()) {
419 if level.format == 0 {
420 return Err(error(
421 "Opaque legacy encoding requires explicit known format; use mips",
422 ));
423 }
424 let mut pixels = vec![0; level.width as usize * level.height as usize * 4];
425 for y in 0..level.height {
426 for x in 0..level.width {
427 let source = (((y as u64 * height as u64 / level.height as u64) as usize
428 * width as usize)
429 + (x as u64 * width as u64 / level.width as u64) as usize)
430 * 4;
431 let target = (y as usize * level.width as usize + x as usize) * 4;
432 pixels[target..target + 4].copy_from_slice(&rgba[source..source + 4]);
433 }
434 }
435 *level = encode_level(level.format, level.width, level.height, &pixels)?;
436 }
437 Ok(Texture {
438 name: String::new(),
439 version: 3,
440 width,
441 height,
442 format,
443 mips: u32::from(mips),
444 levels,
445 fallback_levels,
446 trailing: String::new(),
447 })
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453 #[test]
454 fn texture_containers_and_bc_alpha() -> Result<()> {
455 for format in [DXT1, DXT3, DXT5] {
456 let pixels = [255_u8; 8 * 8 * 4];
457 let texture = create_texture(8, 8, &pixels, format, true)?;
458 let bytes = write_texture(&texture)?;
459 assert_eq!(write_texture(&parse_texture(&bytes)?)?, bytes);
460 let decoded = decode_level(&texture.levels[0])?;
461 assert!(decoded.iter().all(|&channel| channel >= 248));
462 }
463 Ok(())
464 }
465}