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}