Skip to main content

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}