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
60use std::collections::BTreeMap;
61
62use crate::error::Result;
63use crate::binary::{ReadBytes, WriteBytes};
64use crate::files::{DecodeableExtraData, Decodeable, EncodeableExtraData, Encodeable, table::{DecodedData, local::TableInMemory, Table}};
65use crate::schema::{Definition, Field, FieldType};
66use crate::utils::check_size_mismatch;
67
68/// File extension for atlas files.
69pub const EXTENSION: &str = ".atlas";
70
71/// Standard texture atlas size in pixels (4096x4096).
72///
73/// This constant is used to convert normalized texture coordinates (0.0-1.0)
74/// to pixel coordinates within the atlas image.
75const IMAGE_SIZE: u32 = 4096;
76
77/// Atlas file format version.
78const VERSION: i32 = 1;
79
80#[cfg(test)] mod atlas_test;
81
82//---------------------------------------------------------------------------//
83//                              Enum & Structs
84//---------------------------------------------------------------------------//
85
86/// Represents a texture atlas mapping file.
87///
88/// Contains metadata and a list of sprite entries that map logical names to
89/// texture coordinates within an atlas image.
90#[derive(PartialEq, Clone, Debug, Default, Getters, MutGetters, Setters, Serialize, Deserialize)]
91#[getset(get = "pub", get_mut = "pub", set = "pub")]
92pub struct Atlas {
93    /// File format version (currently always 1).
94    version: u32,
95
96    /// Unknown field, purpose not yet identified.
97    unknown: u32,
98
99    /// List of sprite entries defining texture coordinate mappings.
100    entries: Vec<AtlasEntry>,
101}
102
103/// Represents a single sprite entry in an atlas file.
104///
105/// Defines the mapping between a sprite name and its position/size within
106/// the atlas texture. Coordinates are in pixel space (0-4096 range).
107#[derive(PartialEq, Clone, Debug, Default, Getters, MutGetters, Setters, Serialize, Deserialize)]
108#[getset(get = "pub", get_mut = "pub", set = "pub")]
109pub struct AtlasEntry {
110    /// Primary identifier string (sprite name or reference).
111    string1: String,
112
113    /// Secondary identifier string (may be empty or contain additional metadata).
114    string2: String,
115
116    /// X coordinate of the top-left corner in pixels.
117    x_1: f32,
118
119    /// Y coordinate of the top-left corner in pixels.
120    y_1: f32,
121
122    /// X coordinate of the bottom-right corner in pixels.
123    x_2: f32,
124
125    /// Y coordinate of the bottom-right corner in pixels.
126    y_2: f32,
127
128    /// Width of the sprite in pixels.
129    width: f32,
130
131    /// Height of the sprite in pixels.
132    height: f32,
133}
134
135//---------------------------------------------------------------------------//
136//                           Implementation
137//---------------------------------------------------------------------------//
138
139impl From<TableInMemory> for Atlas {
140    fn from(value: TableInMemory) -> Self {
141        let entries = value.data()
142            .iter()
143            .map(|row| AtlasEntry {
144                string1: if let DecodedData::StringU8(data) = &row[0] { data.to_string() } else { panic!("WTF?!")},
145                string2: if let DecodedData::StringU8(data) = &row[1] { data.to_string() } else { panic!("WTF?!")},
146                x_1: if let DecodedData::F32(data) = row[2] { data } else { panic!("WTF?!")},
147                y_1: if let DecodedData::F32(data) = row[3] { data } else { panic!("WTF?!")},
148                x_2: if let DecodedData::F32(data) = row[4] { data } else { panic!("WTF?!")},
149                y_2: if let DecodedData::F32(data) = row[5] { data } else { panic!("WTF?!")},
150                width: if let DecodedData::F32(data) = row[6] { data } else { panic!("WTF?!")},
151                height: if let DecodedData::F32(data) = row[7] { data } else { panic!("WTF?!")},
152            })
153            .collect();
154
155        Self {
156            version: VERSION as u32,
157            unknown: 0,
158            entries,
159        }
160    }
161}
162
163impl From<Atlas> for TableInMemory {
164    fn from(value: Atlas) -> Self {
165        let mut table = Self::new(&Atlas::definition(), None, "");
166        let data = value.entries.iter()
167            .map(|entry| {
168                 vec![
169                    DecodedData::StringU8(entry.string1.to_owned()),
170                    DecodedData::StringU8(entry.string2.to_owned()),
171                    DecodedData::F32(entry.x_1),
172                    DecodedData::F32(entry.y_1),
173                    DecodedData::F32(entry.x_2),
174                    DecodedData::F32(entry.y_2),
175                    DecodedData::F32(entry.width),
176                    DecodedData::F32(entry.height),
177                ]
178            })
179            .collect::<Vec<_>>();
180        let _ = table.set_data(&data);
181        table
182    }
183}
184
185impl Atlas {
186
187    /// Returns the table schema definition for atlas files.
188    ///
189    /// This definition is used when converting atlas files to/from [`TableInMemory`]
190    /// for TSV export/import functionality.
191    ///
192    /// [`TableInMemory`]: crate::files::table::local::TableInMemory
193    ///
194    /// # Returns
195    ///
196    /// A [`Definition`] with 8 fields matching the [`AtlasEntry`] structure:
197    /// - `string1`, `string2`: String identifiers
198    /// - `x_1`, `y_1`, `x_2`, `y_2`: Coordinate floats
199    /// - `width`, `height`: Dimension floats
200    pub fn definition() -> Definition {
201        let mut definition = Definition::new(VERSION, None);
202        let fields = vec![
203            Field::new("string1".to_owned(), FieldType::StringU8, true, Some("PLACEHOLDER".to_owned()), false, None, None, None, String::new(), 0, 0, BTreeMap::new(), None),
204            Field::new("string2".to_owned(), FieldType::StringU8, true, Some("PLACEHOLDER".to_owned()), false, None, None, None, String::new(), 0, 0, BTreeMap::new(), None),
205            Field::new("x_1".to_owned(), FieldType::F32, false, Some("0".to_owned()), false, None, None, None, String::new(), 0, 0, BTreeMap::new(), None),
206            Field::new("y_1".to_owned(), FieldType::F32, false, Some("0".to_owned()), false, None, None, None, String::new(), 0, 0, BTreeMap::new(), None),
207            Field::new("x_2".to_owned(), FieldType::F32, false, Some("0".to_owned()), false, None, None, None, String::new(), 0, 0, BTreeMap::new(), None),
208            Field::new("y_2".to_owned(), FieldType::F32, false, Some("0".to_owned()), false, None, None, None, String::new(), 0, 0, BTreeMap::new(), None),
209            Field::new("width".to_owned(), FieldType::F32, false, Some("0".to_owned()), false, None, None, None, String::new(), 0, 0, BTreeMap::new(), None),
210            Field::new("height".to_owned(), FieldType::F32, false, Some("0".to_owned()), false, None, None, None, String::new(), 0, 0, BTreeMap::new(), None),
211        ];
212        definition.set_fields(fields);
213        definition
214    }
215}
216
217impl Decodeable for Atlas {
218
219    fn decode<R: ReadBytes>(data: &mut R, _extra_data: &Option<DecodeableExtraData>) -> Result<Self> {
220        let version = data.read_u32()?;
221        let unknown = data.read_u32()?;
222
223        let mut entries = vec![];
224
225        for _ in 0..data.read_u32()? {
226
227            // The coordinates are stored in percentage of size.
228            entries.push(AtlasEntry {
229                string1: data.read_string_u16_0padded(512)?,
230                string2: data.read_string_u16_0padded(512)?,
231                x_1: data.read_f32()? * IMAGE_SIZE as f32,
232                y_1: data.read_f32()? * IMAGE_SIZE as f32,
233                x_2: data.read_f32()? * IMAGE_SIZE as f32,
234                y_2: data.read_f32()? * IMAGE_SIZE as f32,
235                width: data.read_f32()?,
236                height: data.read_f32()?,
237            })
238        }
239
240        // Trigger an error if there's left data on the source.
241        check_size_mismatch(data.stream_position()? as usize, data.len()? as usize)?;
242
243        Ok(Self {
244            version,
245            unknown,
246            entries
247        })
248    }
249}
250
251impl Encodeable for Atlas {
252
253    fn encode<W: WriteBytes>(&mut self, buffer: &mut W, _extra_data: &Option<EncodeableExtraData>) -> Result<()> {
254        buffer.write_u32(self.version)?;
255        buffer.write_u32(self.unknown)?;
256        buffer.write_u32(self.entries.len() as u32)?;
257
258        for entry in &self.entries {
259            buffer.write_string_u16_0padded(&entry.string1, 512, true)?;
260            buffer.write_string_u16_0padded(&entry.string2, 512, true)?;
261            buffer.write_f32(entry.x_1 / IMAGE_SIZE as f32)?;
262            buffer.write_f32(entry.y_1 / IMAGE_SIZE as f32)?;
263            buffer.write_f32(entry.x_2 / IMAGE_SIZE as f32)?;
264            buffer.write_f32(entry.y_2 / IMAGE_SIZE as f32)?;
265            buffer.write_f32(entry.width)?;
266            buffer.write_f32(entry.height)?;
267        }
268
269        Ok(())
270    }
271}