rpfm_lib/files/bmd_vegetation/tree_list/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//! Tree placement list for BMD vegetation files.
12//!
13//! This module defines the tree placement data structures used in BMD vegetation files.
14//! Trees are organized by their model path, with each model containing multiple
15//! individual tree instances with positions, rotations, and scales.
16//!
17//! # Structure
18//!
19//! - [`TreeList`]: Top-level container for all tree placement data
20//! - [`BattleTreeItemVector`]: Groups trees by their model path
21//! - [`BattleTreeItem`]: Individual tree instance with transform data
22//!
23//! # Supported Versions
24//!
25//! - **Versions 1 to 3**: Total War: Attila (scale as a byte, no rotation, version 2 adds `is_burnable`)
26//! - **Version 4**: Current format used in Total War: Warhammer III
27//!
28//! # Examples
29//!
30//! ```rust,ignore
31//! use rpfm_lib::files::bmd_vegetation::tree_list::*;
32//!
33//! // Decode a tree list from binary data
34//! let tree_list = TreeList::decode(&mut data, &extra_data)?;
35//!
36//! // Iterate through tree models
37//! for tree_vector in tree_list.tree_list() {
38//! println!("Model: {}", tree_vector.key());
39//! for tree in tree_vector.value() {
40//! println!(" Position: ({}, {}, {})", tree.x(), tree.y(), tree.z());
41//! }
42//! }
43//! ```
44
45use getset::*;
46use rand::RngExt;
47use serde_derive::{Serialize, Deserialize};
48
49use crate::binary::{ReadBytes, WriteBytes};
50use crate::error::{Result, RLibError};
51use crate::files::{Bmd, bmd::ToLayer, Decodeable, EncodeableExtraData, Encodeable};
52
53use super::DecodeableExtraData;
54
55mod v1;
56mod v4;
57
58//---------------------------------------------------------------------------//
59// Enum & Structs
60//---------------------------------------------------------------------------//
61
62/// Tree placement list for a battle map.
63///
64/// Contains all tree instances organized by their model paths. Each model path
65/// maps to a vector of individual tree instances with transform data.
66///
67/// # Fields
68///
69/// * `serialise_version` - File format version (1 to 3 in Attila, 4 in later games)
70/// * `tree_list` - Vector of tree groups, each containing instances of a specific tree model
71///
72/// # Examples
73///
74/// ```rust,ignore
75/// let tree_list = TreeList::decode(&mut data, &extra_data)?;
76/// println!("Tree models: {}", tree_list.tree_list().len());
77/// ```
78#[derive(Default, PartialEq, Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)]
79#[getset(get = "pub", get_mut = "pub", set = "pub")]
80pub struct TreeList {
81 /// File format version number.
82 serialise_version: u16,
83
84 /// List of tree instance groups, organized by model path.
85 tree_list: Vec<BattleTreeItemVector>,
86}
87
88/// A group of tree instances sharing the same model.
89///
90/// Maps a tree model path (`.rigid_model_v2` file) to a vector of individual
91/// tree instances that use that model.
92///
93/// # Fields
94///
95/// * `key` - Path to the tree model file (e.g., `BattleTerrain/vegetation/trees/wh_palm/wh_lizardmen_tree_palmbig_e.rigid_model_v2`)
96/// * `value` - Vector of individual tree instances using this model
97///
98/// # Examples
99///
100/// ```rust,ignore
101/// for tree_vector in tree_list.tree_list() {
102/// println!("Model: {} ({} instances)", tree_vector.key(), tree_vector.value().len());
103/// }
104/// ```
105#[derive(Default, PartialEq, Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)]
106#[getset(get = "pub", get_mut = "pub", set = "pub")]
107pub struct BattleTreeItemVector {
108 /// Path to the tree model file.
109 key: String,
110
111 /// Vector of tree instances using this model.
112 value: Vec<BattleTreeItem>,
113}
114
115/// Individual tree instance with position, rotation, and scale.
116///
117/// Represents a single tree placed on the battle map with its transform data.
118/// The rotation is compressed into a single byte representing Y-axis rotation.
119///
120/// # Fields
121///
122/// * `id` - Unique identifier for this tree instance
123/// * `x` - X coordinate in world space
124/// * `y` - Y coordinate in world space (height)
125/// * `z` - Z coordinate in world space
126/// * `rotation` - Y-axis rotation compressed to 0-255 range (multiply by 1.40625 to get degrees)
127/// * `scale` - Uniform scale factor applied to all axes
128/// * `flags` - Behavior flags for this tree instance
129///
130/// # Examples
131///
132/// ```rust,ignore
133/// let tree = BattleTreeItem {
134/// id: 0x1609e7e52919a40,
135/// x: -22.92,
136/// y: 58.75,
137/// z: -3.01,
138/// rotation: 72, // ~102 degrees when converted
139/// scale: 1.0,
140/// flags: 3,
141/// };
142/// ```
143#[derive(PartialEq, Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)]
144#[getset(get = "pub", get_mut = "pub", set = "pub")]
145pub struct BattleTreeItem {
146 /// Unique identifier for this tree instance.
147 id: u64,
148
149 /// X coordinate in world space.
150 x: f32,
151
152 /// Y coordinate in world space (height).
153 y: f32,
154
155 /// Z coordinate in world space.
156 z: f32,
157
158 /// Y-axis rotation (0-255 maps to 0-360 degrees). Version 4 only.
159 ///
160 /// To convert to degrees: `rotation as f32 * 1.40625`
161 rotation: u8,
162
163 /// Uniform scale factor. In versions 1 to 3 it is stored as a byte, 127 being 1.0.
164 scale: f32,
165
166 /// Behavior flags for this tree instance. In versions 1 to 3 the byte is the tree's `is_freeform`: 1 keeps the stored height, 0 puts
167 /// the tree on the terrain.
168 flags: u8,
169
170 /// Version 2 only: whether the tree can burn. Always 1 in Attila.
171 is_burnable: u8,
172}
173
174//---------------------------------------------------------------------------//
175// Implementation of TreeList
176//---------------------------------------------------------------------------//
177
178impl Decodeable for TreeList {
179
180 fn decode<R: ReadBytes>(data: &mut R, extra_data: &Option<DecodeableExtraData>) -> Result<Self> {
181 let mut decoded = Self::default();
182 decoded.serialise_version = data.read_u16()?;
183
184 match decoded.serialise_version {
185 1..=3 => decoded.read_v1_to_v3(data, extra_data)?,
186 4 => decoded.read_v4(data, extra_data)?,
187 _ => return Err(RLibError::DecodingFastBinUnsupportedVersion(String::from("TreeList"), decoded.serialise_version)),
188 }
189
190 Ok(decoded)
191 }
192}
193
194impl Encodeable for TreeList {
195
196 fn encode<W: WriteBytes>(&mut self, buffer: &mut W, extra_data: &Option<EncodeableExtraData>) -> Result<()> {
197 buffer.write_u16(self.serialise_version)?;
198
199 match self.serialise_version {
200 1..=3 => self.write_v1_to_v3(buffer, extra_data)?,
201 4 => self.write_v4(buffer, extra_data)?,
202 _ => return Err(RLibError::EncodingFastBinUnsupportedVersion(String::from("TreeList"), self.serialise_version)),
203 }
204
205 Ok(())
206 }
207}
208
209impl Default for BattleTreeItem {
210 fn default() -> Self {
211 Self {
212 id: rand::rng().random::<u64>(),
213 x: f32::default(),
214 y: f32::default(),
215 z: f32::default(),
216 rotation: u8::default(),
217 scale: f32::default(),
218 flags: u8::default(),
219 is_burnable: u8::default(),
220 }
221 }
222}
223
224impl ToLayer for TreeList {
225 fn to_layer(&self, _parent: &Bmd) -> Result<String> {
226 let mut layer = String::new();
227/*
228 <BATTLE_TREE_ITEM_VECTOR key='BattleTerrain/vegetation/trees/wh_palm/wh_lizardmen_tree_palmbig_e.rigid_model_v2'>
229 <value>
230 <BATTLE_TREE_ITEM x='13.430786' y='56.218090' z='64.390381' rotation='72' scale='1.000000' flags='3'/>
231 <BATTLE_TREE_ITEM x='30.591064' y='47.634796' z='64.003784' rotation='72' scale='1.000000' flags='3'/>
232 <BATTLE_TREE_ITEM x='-58.119385' y='28.191170' z='-7.750488' rotation='72' scale='1.000000' flags='3'/>
233 <BATTLE_TREE_ITEM x='-54.575928' y='44.780090' z='19.081787' rotation='72' scale='1.000000' flags='3'/>
234 <BATTLE_TREE_ITEM x='47.375000' y='40.378334' z='-42.304565' rotation='72' scale='1.000000' flags='3'/>
235 <BATTLE_TREE_ITEM x='-46.924316' y='29.631201' z='-61.144165' rotation='72' scale='1.000000' flags='3'/>
236 <BATTLE_TREE_ITEM x='9.264160' y='63.425400' z='56.489380' rotation='72' scale='1.000000' flags='3'/>
237 <BATTLE_TREE_ITEM x='29.056519' y='39.673546' z='-42.304565' rotation='72' scale='1.000000' flags='3'/>
238 <BATTLE_TREE_ITEM x='-41.629883' y='58.078644' z='40.268311' rotation='72' scale='1.000000' flags='3'/>
239 <BATTLE_TREE_ITEM x='-25.574707' y='43.598698' z='-44.591797' rotation='72' scale='1.000000' flags='3'/>
240 <BATTLE_TREE_ITEM x='48.763184' y='63.282581' z='18.791626' rotation='72' scale='1.000000' flags='3'/>
241 <BATTLE_TREE_ITEM x='-65.662598' y='23.835663' z='-43.105835' rotation='72' scale='1.000000' flags='3'/>
242 <BATTLE_TREE_ITEM x='62.529541' y='64.074753' z='-10.465454' rotation='72' scale='1.000000' flags='3'/>
243 <BATTLE_TREE_ITEM x='-10.322266' y='58.416050' z='49.374023' rotation='72' scale='1.000000' flags='3'/>
244 <BATTLE_TREE_ITEM x='59.566162' y='41.931854' z='-31.586426' rotation='72' scale='1.000000' flags='3'/>
245 <BATTLE_TREE_ITEM x='44.394775' y='28.780624' z='-50.351074' rotation='72' scale='1.000000' flags='3'/>
246 <BATTLE_TREE_ITEM x='55.461060' y='48.186008' z='30.768555' rotation='72' scale='1.000000' flags='3'/>
247 <BATTLE_TREE_ITEM x='-22.920776' y='58.752563' z='-3.012939' rotation='72' scale='1.000000' flags='3'/>
248 <BATTLE_TREE_ITEM x='47.971313' y='72.914719' z='2.633179' rotation='72' scale='1.000000' flags='3'/>
249 <BATTLE_TREE_ITEM x='18.252930' y='62.910236' z='59.119873' rotation='72' scale='1.000000' flags='3'/>
250 <BATTLE_TREE_ITEM x='5.463135' y='39.673546' z='-29.382080' rotation='72' scale='1.000000' flags='3'/>
251 <BATTLE_TREE_ITEM x='-29.859375' y='66.881577' z='53.766235' rotation='72' scale='1.000000' flags='3'/>
252 <BATTLE_TREE_ITEM x='-15.012939' y='41.917336' z='-44.591797' rotation='72' scale='1.000000' flags='3'/>
253 <BATTLE_TREE_ITEM x='-2.984741' y='56.629513' z='67.660400' rotation='72' scale='1.000000' flags='3'/>
254 <BATTLE_TREE_ITEM x='-69.040771' y='24.034649' z='-32.138916' rotation='72' scale='1.000000' flags='3'/>
255 <BATTLE_TREE_ITEM x='34.823364' y='60.135620' z='49.923828' rotation='72' scale='1.000000' flags='3'/>
256 <BATTLE_TREE_ITEM x='55.175659' y='68.957443' z='6.442505' rotation='72' scale='1.000000' flags='3'/>
257 <BATTLE_TREE_ITEM x='-27.754395' y='48.756340' z='6.032227' rotation='72' scale='1.000000' flags='3'/>
258 <BATTLE_TREE_ITEM x='-46.053345' y='54.695683' z='19.081787' rotation='72' scale='1.000000' flags='3'/>
259 <BATTLE_TREE_ITEM x='-16.399902' y='58.416046' z='61.042847' rotation='72' scale='1.000000' flags='3'/>
260 <BATTLE_TREE_ITEM x='-35.505859' y='64.935822' z='43.448242' rotation='72' scale='1.000000' flags='3'/>
261 <BATTLE_TREE_ITEM x='50.590454' y='48.186008' z='36.953979' rotation='72' scale='1.000000' flags='3'/>
262 <BATTLE_TREE_ITEM x='-22.920776' y='58.752563' z='27.214111' rotation='72' scale='1.000000' flags='3'/>
263 <BATTLE_TREE_ITEM x='-36.628418' y='41.561756' z='-20.399292' rotation='72' scale='1.000000' flags='3'/>
264 <BATTLE_TREE_ITEM x='-22.920654' y='58.752567' z='-12.470703' rotation='72' scale='1.000000' flags='3'/>
265 <BATTLE_TREE_ITEM x='-41.240234' y='43.078796' z='-7.750366' rotation='72' scale='1.000000' flags='3'/>
266 <BATTLE_TREE_ITEM x='-32.357910' y='30.178255' z='-67.639404' rotation='72' scale='1.000000' flags='3'/>
267 <BATTLE_TREE_ITEM x='-15.012939' y='41.917336' z='-44.591797' rotation='72' scale='1.000000' flags='3'/>
268 </value>
269
270 <entity id="1609e7e52919a40">
271 <ECVegetation key="BattleTerrain/vegetation/trees/wh_palm/wh_lizardmen_tree_palmbig_e.rigid_model_v2" legacy_random_rotation="true"/>
272 <ECTransform position="-22.9207764 58.7525635 -3.01293945" rotation="0 102.000023 0" scale="1 1 1" pivot="0 0 0"/>
273 <ECTerrainClamp active="false" clamp_to_sea_level="false" terrain_oriented="false" fit_height_to_terrain="false"/>
274 </entity>*/
275
276 for item_vector in self.tree_list() {
277 for item in item_vector.value() {
278
279 layer.push_str(&format!("
280 <entity id=\"{:x}\">",
281 item.id())
282 );
283
284 layer.push_str(&format!("
285 <ECVegetation key=\"{}\" legacy_random_rotation=\"true\"/>",
286 item_vector.key(),
287 ));
288
289 layer.push_str(&format!("
290 <ECTransform position=\"{:.5} {:.5} {:.5}\" rotation=\"{:.5} {:.5} {:.5}\" scale=\"{:.5} {:.5} {:.5}\" pivot=\"0 0 0\"/>",
291 item.x(),
292 item.y(),
293 item.z(),
294 0,
295 *item.rotation() as f32 * 1.40625, // Rotation is only Y-axis, divided by 1.4 to fit a full 360 degree rotation in one byte.
296 0,
297 item.scale(),
298 item.scale(),
299 item.scale(),
300 ));
301
302 layer.push_str("
303 <ECTerrainClamp active=\"false\" clamp_to_sea_level=\"false\" terrain_oriented=\"false\" fit_height_to_terrain=\"false\"/>");
304
305 layer.push_str("
306 </entity>"
307 );
308 }
309 }
310
311 Ok(layer)
312 }
313}