Skip to main content

rpfm_lib/files/atlas/
mod.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! Atlas texture coordinate mapping file format support.
12//!
13//! This module handles `.atlas` files which define texture coordinate mappings for UI sprites
14//! and other 2D graphics elements in Total War games. Atlas files map logical sprite names
15//! to rectangular regions within larger texture atlas images.
16//!
17//! # File Format
18//!
19//! Atlas files use a binary format with the following structure:
20//! - Header with version and metadata
21//! - List of atlas entries mapping sprites to texture coordinates
22//! - Coordinates are stored as percentages of the atlas texture size (4096x4096)
23//!
24//! # Coordinate System
25//!
26//! Texture coordinates are stored as normalized values (0.0-1.0 range) and converted to
27//! pixel coordinates by multiplying by `IMAGE_SIZE` (4096). Each entry defines:
28//! - Top-left corner (x1, y1)
29//! - Bottom-right corner (x2, y2)
30//! - Sprite dimensions (width, height)
31//!
32//! # Table Conversion
33//!
34//! Atlas files can be converted to/from [`TableInMemory`] for easy editing as TSV files.
35//! The table format has 8 columns matching the [`AtlasEntry`] fields.
36//!
37//! [`TableInMemory`]: crate::files::table::local::TableInMemory
38//!
39//! # Usage
40//!
41//! ```ignore
42//! use rpfm_lib::files::atlas::Atlas;
43//! use rpfm_lib::files::Decodeable;
44//!
45//! // Decode an atlas file
46//! let atlas = Atlas::decode(&mut data, &None)?;
47//!
48//! // Access entries
49//! for entry in atlas.entries() {
50//!     println!("Sprite: {} at ({}, {})", entry.string1(), entry.x_1(), entry.y_1());
51//! }
52//!
53//! // Convert to table for TSV export
54//! let table = TableInMemory::from(atlas);
55//! ```
56
57use getset::*;
58use serde_derive::{Serialize, Deserialize};
59
60
61use crate::error::Result;
62use crate::binary::{ReadBytes, WriteBytes};
63use crate::files::{DecodeableExtraData, Decodeable, EncodeableExtraData, Encodeable, table::{DecodedData, local::TableInMemory, Table}};
64use crate::schema::{Definition, Field, FieldType};
65use crate::utils::check_size_mismatch;
66
67/// File extension for atlas files.
68pub const EXTENSION: &str = ".atlas";
69
70/// Standard texture atlas size in pixels (4096x4096).
71///
72/// This constant is used to convert normalized texture coordinates (0.0-1.0)
73/// to pixel coordinates within the atlas image.
74const IMAGE_SIZE: u32 = 4096;
75
76/// Atlas file format version.
77const VERSION: i32 = 1;
78
79#[cfg(test)] mod atlas_test;
80
81//---------------------------------------------------------------------------//
82//                              Enum & Structs
83//---------------------------------------------------------------------------//
84
85/// Represents a texture atlas mapping file.
86///
87/// Contains metadata and a list of sprite entries that map logical names to
88/// texture coordinates within an atlas image.
89#[derive(PartialEq, Clone, Debug, Default, Getters, MutGetters, Setters, Serialize, Deserialize)]
90#[getset(get = "pub", get_mut = "pub", set = "pub")]
91pub struct Atlas {
92    /// File format version (currently always 1).
93    version: u32,
94
95    /// Unknown field, purpose not yet identified.
96    unknown: u32,
97
98    /// List of sprite entries defining texture coordinate mappings.
99    entries: Vec<AtlasEntry>,
100}
101
102/// Represents a single sprite entry in an atlas file.
103///
104/// Defines the mapping between a sprite name and its position/size within
105/// the atlas texture. Coordinates are in pixel space (0-4096 range).
106#[derive(PartialEq, Clone, Debug, Default, Getters, MutGetters, Setters, Serialize, Deserialize)]
107#[getset(get = "pub", get_mut = "pub", set = "pub")]
108pub struct AtlasEntry {
109    /// Primary identifier string (sprite name or reference).
110    string1: String,
111
112    /// Secondary identifier string (may be empty or contain additional metadata).
113    string2: String,
114
115    /// X coordinate of the top-left corner in pixels.
116    x_1: f32,
117
118    /// Y coordinate of the top-left corner in pixels.
119    y_1: f32,
120
121    /// X coordinate of the bottom-right corner in pixels.
122    x_2: f32,
123
124    /// Y coordinate of the bottom-right corner in pixels.
125    y_2: f32,
126
127    /// Width of the sprite in pixels.
128    width: f32,
129
130    /// Height of the sprite in pixels.
131    height: f32,
132}
133
134//---------------------------------------------------------------------------//
135//                           Implementation
136//---------------------------------------------------------------------------//
137
138impl From<TableInMemory> for Atlas {
139    fn from(value: TableInMemory) -> Self {
140        let entries = value.data()
141            .iter()
142            .map(|row| AtlasEntry {
143                string1: if let DecodedData::StringU8(data) = &row[0] { data.to_string() } else { panic!("WTF?!")},
144                string2: if let DecodedData::StringU8(data) = &row[1] { data.to_string() } else { panic!("WTF?!")},
145                x_1: if let DecodedData::F32(data) = row[2] { data } else { panic!("WTF?!")},
146                y_1: if let DecodedData::F32(data) = row[3] { data } else { panic!("WTF?!")},
147                x_2: if let DecodedData::F32(data) = row[4] { data } else { panic!("WTF?!")},
148                y_2: if let DecodedData::F32(data) = row[5] { data } else { panic!("WTF?!")},
149                width: if let DecodedData::F32(data) = row[6] { data } else { panic!("WTF?!")},
150                height: if let DecodedData::F32(data) = row[7] { data } else { panic!("WTF?!")},
151            })
152            .collect();
153
154        Self {
155            version: VERSION as u32,
156            unknown: 0,
157            entries,
158        }
159    }
160}
161
162impl From<Atlas> for TableInMemory {
163    fn from(value: Atlas) -> Self {
164        let mut table = Self::new(&Atlas::definition(), None, "");
165        let data = value.entries.iter()
166            .map(|entry| {
167                 vec![
168                    DecodedData::StringU8(entry.string1.to_owned()),
169                    DecodedData::StringU8(entry.string2.to_owned()),
170                    DecodedData::F32(entry.x_1),
171                    DecodedData::F32(entry.y_1),
172                    DecodedData::F32(entry.x_2),
173                    DecodedData::F32(entry.y_2),
174                    DecodedData::F32(entry.width),
175                    DecodedData::F32(entry.height),
176                ]
177            })
178            .collect::<Vec<_>>();
179        let _ = table.set_data(&data);
180        table
181    }
182}
183
184impl Atlas {
185
186    /// Returns the table schema definition for atlas files.
187    ///
188    /// This definition is used when converting atlas files to/from [`TableInMemory`]
189    /// for TSV export/import functionality.
190    ///
191    /// [`TableInMemory`]: crate::files::table::local::TableInMemory
192    ///
193    /// # Returns
194    ///
195    /// A [`Definition`] with 8 fields matching the [`AtlasEntry`] structure:
196    /// - `string1`, `string2`: String identifiers
197    /// - `x_1`, `y_1`, `x_2`, `y_2`: Coordinate floats
198    /// - `width`, `height`: Dimension floats
199    pub fn definition() -> Definition {
200        let mut definition = Definition::new(VERSION, None);
201        let fields = vec![
202            Field { name: "string1".to_owned(), is_key: true, default_value: Some("PLACEHOLDER".to_owned()), ..Default::default() },
203            Field { name: "string2".to_owned(), is_key: true, default_value: Some("PLACEHOLDER".to_owned()), ..Default::default() },
204            Field { name: "x_1".to_owned(), field_type: FieldType::F32, default_value: Some("0".to_owned()), ..Default::default() },
205            Field { name: "y_1".to_owned(), field_type: FieldType::F32, default_value: Some("0".to_owned()), ..Default::default() },
206            Field { name: "x_2".to_owned(), field_type: FieldType::F32, default_value: Some("0".to_owned()), ..Default::default() },
207            Field { name: "y_2".to_owned(), field_type: FieldType::F32, default_value: Some("0".to_owned()), ..Default::default() },
208            Field { name: "width".to_owned(), field_type: FieldType::F32, default_value: Some("0".to_owned()), ..Default::default() },
209            Field { name: "height".to_owned(), field_type: FieldType::F32, default_value: Some("0".to_owned()), ..Default::default() },
210        ];
211        definition.set_fields(fields);
212        definition
213    }
214}
215
216impl Decodeable for Atlas {
217
218    fn decode<R: ReadBytes>(data: &mut R, _extra_data: &Option<DecodeableExtraData>) -> Result<Self> {
219        let version = data.read_u32()?;
220        let unknown = data.read_u32()?;
221
222        let mut entries = vec![];
223
224        for _ in 0..data.read_u32()? {
225
226            // The coordinates are stored in percentage of size.
227            entries.push(AtlasEntry {
228                string1: data.read_string_u16_0padded(512)?,
229                string2: data.read_string_u16_0padded(512)?,
230                x_1: data.read_f32()? * IMAGE_SIZE as f32,
231                y_1: data.read_f32()? * IMAGE_SIZE as f32,
232                x_2: data.read_f32()? * IMAGE_SIZE as f32,
233                y_2: data.read_f32()? * IMAGE_SIZE as f32,
234                width: data.read_f32()?,
235                height: data.read_f32()?,
236            })
237        }
238
239        // Trigger an error if there's left data on the source.
240        check_size_mismatch(data.stream_position()? as usize, data.len()? as usize)?;
241
242        Ok(Self {
243            version,
244            unknown,
245            entries
246        })
247    }
248}
249
250impl Encodeable for Atlas {
251
252    fn encode<W: WriteBytes>(&mut self, buffer: &mut W, _extra_data: &Option<EncodeableExtraData>) -> Result<()> {
253        buffer.write_u32(self.version)?;
254        buffer.write_u32(self.unknown)?;
255        buffer.write_u32(self.entries.len() as u32)?;
256
257        for entry in &self.entries {
258            buffer.write_string_u16_0padded(&entry.string1, 512, true)?;
259            buffer.write_string_u16_0padded(&entry.string2, 512, true)?;
260            buffer.write_f32(entry.x_1 / IMAGE_SIZE as f32)?;
261            buffer.write_f32(entry.y_1 / IMAGE_SIZE as f32)?;
262            buffer.write_f32(entry.x_2 / IMAGE_SIZE as f32)?;
263            buffer.write_f32(entry.y_2 / IMAGE_SIZE as f32)?;
264            buffer.write_f32(entry.width)?;
265            buffer.write_f32(entry.height)?;
266        }
267
268        Ok(())
269    }
270}