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}