Skip to main content

rpfm_lib/files/portrait_settings/
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//! Portrait settings for unit and character portraits.
12//!
13//! This module handles binary portrait settings files that define camera positions
14//! and rendering parameters for unit portraits in Total War games. These portraits
15//! appear in various places:
16//! - Unit cards at the bottom of the battle/campaign UI
17//! - Character details windows in campaign
18//! - Diplomacy screens
19//!
20//! # Supported Versions
21//!
22//! - **Version 1**: Used in Warhammer 2, Warhammer 1, Thrones of Britannia, and Attila
23//! - **Version 4**: Used in Warhammer 3
24//!
25//! # File Location
26//!
27//! Portrait settings files are typically found at `ui/portraits/` within game packs.
28
29use getset::*;
30use serde_derive::{Serialize, Deserialize};
31
32use crate::error::{Result, RLibError};
33use crate::binary::{ReadBytes, WriteBytes};
34use crate::files::{DecodeableExtraData, Decodeable, EncodeableExtraData, Encodeable};
35use crate::utils::check_size_mismatch;
36
37/// Extension used by portrait settings files.
38pub const EXTENSION: &str = ".bin";
39
40mod versions;
41
42#[cfg(test)] mod portrait_settings_test;
43
44//---------------------------------------------------------------------------//
45//                              Enum & Structs
46//---------------------------------------------------------------------------//
47
48/// Portrait settings file containing camera configurations for unit portraits.
49///
50/// Each entry in this file corresponds to an art set and defines how the camera
51/// should be positioned when rendering that unit's portrait.
52#[derive(PartialEq, Clone, Debug, Default, Getters, MutGetters, Setters, Serialize, Deserialize)]
53#[getset(get = "pub", get_mut = "pub", set = "pub")]
54pub struct PortraitSettings {
55
56    /// Format version of this file (1 or 4).
57    version: u32,
58
59    /// Portrait entries, one per art set.
60    entries: Vec<Entry>,
61}
62
63/// A portrait entry defining camera settings for a specific art set.
64///
65/// Each entry links an art set ID to camera configurations for rendering
66/// head and optionally full-body portraits.
67#[derive(PartialEq, Clone, Debug, Default, Getters, MutGetters, Setters, Serialize, Deserialize)]
68#[getset(get = "pub", get_mut = "pub", set = "pub")]
69pub struct Entry {
70
71    /// Art set key this entry applies to.
72    ///
73    /// References a key in the art set tables (e.g., `land_units_tables`).
74    id: String,
75
76    /// Camera settings for the head/portrait view.
77    ///
78    /// This is the porthole camera used for unit cards in the bottom-left UI area.
79    camera_settings_head: CameraSetting,
80
81    /// Camera settings for the full-body view (optional).
82    ///
83    /// Used in character detail windows in campaign. Only needed for characters
84    /// and heroes; regular units don't require body camera settings.
85    camera_settings_body: Option<CameraSetting>,
86
87    /// Texture variants for this portrait.
88    ///
89    /// Allows different textures to be used based on conditions like season,
90    /// character level, or faction role.
91    variants: Vec<Variant>
92}
93
94/// Camera positioning and field-of-view settings for a portrait.
95///
96/// Defines how the camera is positioned relative to the character model when
97/// rendering a portrait. The camera has an auto-level feature that compensates
98/// for vertical rotation (pitch) exceeding 90/-90 degrees.
99#[derive(PartialEq, Clone, Debug, Default, Getters, MutGetters, Setters, Serialize, Deserialize)]
100#[getset(get = "pub", get_mut = "pub", set = "pub")]
101pub struct CameraSetting {
102
103    /// Distance from the character along the Z axis (depth).
104    z: f32,
105
106    /// Vertical displacement of the camera (height offset).
107    y: f32,
108
109    /// Horizontal rotation angle in degrees (left/right).
110    yaw: f32,
111
112    /// Vertical rotation angle in degrees (up/down).
113    pitch: f32,
114
115    /// Camera distance. Only used in version 1.
116    distance: f32,
117
118    /// Spherical coordinate theta. Only used in version 1.
119    theta: f32,
120
121    /// Spherical coordinate phi. Only used in version 1.
122    phi: f32,
123
124    /// Field of view angle in degrees.
125    fov: f32,
126
127    /// Skeleton bone to use as the camera focus point.
128    ///
129    /// If specified, all camera offsets and rotations are relative to this bone's
130    /// position. Common values include head or chest bones.
131    skeleton_node: String,
132}
133
134/// A texture variant for a portrait entry.
135///
136/// Variants allow different portrait textures to be used based on game conditions
137/// such as season, character level, age, or faction role.
138#[derive(PartialEq, Eq, Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)]
139#[getset(get = "pub", get_mut = "pub", set = "pub")]
140pub struct Variant {
141
142    /// Variant identifier matching the `variant_filename` column in variants tables.
143    filename: String,
144
145    /// Path to the diffuse (color) texture for this variant.
146    file_diffuse: String,
147
148    /// Path to first mask texture (purpose unknown).
149    file_mask_1: String,
150
151    /// Path to second mask texture (purpose unknown).
152    file_mask_2: String,
153
154    /// Path to third mask texture (purpose unknown).
155    file_mask_3: String,
156
157    /// Season when this variant applies. Only used in version 1.
158    season: String,
159
160    /// Character level threshold. Only used in version 1.
161    level: i32,
162
163    /// Character age threshold. Only used in version 1.
164    age: i32,
165
166    /// Whether this variant is for politicians. Only used in version 1.
167    politician: bool,
168
169    /// Whether this variant is for faction leaders. Only used in version 1.
170    faction_leader: bool,
171}
172
173//---------------------------------------------------------------------------//
174//                       Implementation of PortraitSettings
175//---------------------------------------------------------------------------//
176
177impl Decodeable for PortraitSettings {
178
179    fn decode<R: ReadBytes>(data: &mut R, _extra_data: &Option<DecodeableExtraData>) -> Result<Self> {
180        let version = data.read_u32()?;
181
182        let mut settings = Self::default();
183        settings.version = version;
184
185        match version {
186            1 => settings.read_v1(data)?,
187            4 => settings.read_v4(data)?,
188            _ => Err(RLibError::DecodingPortraitSettingUnsupportedVersion(version as usize))?,
189        }
190
191        // Trigger an error if there's left data on the source.
192        check_size_mismatch(data.stream_position()? as usize, data.len()? as usize)?;
193
194        Ok(settings)
195    }
196}
197
198impl Encodeable for PortraitSettings {
199
200    fn encode<W: WriteBytes>(&mut self, buffer: &mut W, _extra_data: &Option<EncodeableExtraData>) -> Result<()> {
201        buffer.write_u32(self.version)?;
202
203        match self.version {
204            1 => self.write_v1(buffer)?,
205            4 => self.write_v4(buffer)?,
206            _ => unimplemented!()
207        }
208
209        Ok(())
210    }
211}
212
213
214impl PortraitSettings {
215
216    /// Deserializes portrait settings from a JSON string.
217    pub fn from_json(data: &str) -> Result<Self> {
218        serde_json::from_str(data).map_err(From::from)
219    }
220
221    /// Serializes this portrait settings to a pretty-printed JSON string.
222    pub fn to_json(&self) -> Result<String> {
223        serde_json::to_string_pretty(&self).map_err(From::from)
224    }
225}
226
227impl Default for Variant {
228    fn default() -> Self {
229        Self {
230            filename: Default::default(),
231            file_diffuse: Default::default(),
232            file_mask_1: Default::default(),
233            file_mask_2: Default::default(),
234            file_mask_3: Default::default(),
235            season: "none".to_owned(),
236            level: Default::default(),
237            age: Default::default(),
238            politician: Default::default(),
239            faction_leader: Default::default(),
240        }
241    }
242}