Skip to main content

rpfm_lib/files/bmd/
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//! Battle Map Definition (BMD) file format support.
12//!
13//! BMD files (`.bmd`) are FASTBIN0-format files that define battle map layouts for Total War
14//! games. They contain comprehensive 3D scene data including buildings, terrain, lighting,
15//! vegetation, deployment zones, AI hints, and other gameplay-critical map elements.
16//!
17//! # File Format
18//!
19//! BMD files use the FASTBIN0 binary format with a version-specific structure:
20//!
21//! ```text
22//! [8 bytes]  FASTBIN0 signature
23//! [u16]      serialise_version
24//! [...]      version-specific data
25//! ```
26//!
27//! # Supported Versions
28//!
29//! All currently supported versions are:
30//! - **Version 23**
31//! - **Version 24**
32//! - **Version 25**
33//! - **Version 26**
34//! - **Version 27**
35//!
36//! # File Contents
37//!
38//! BMD files contain numerous specialized data lists:
39//!
40//! ## Buildings & Structures
41//! - `BattlefieldBuildingList` - Buildings inside the battlefield area
42//! - `BattlefieldBuildingListFar` - Buildings outside the battlefield area
43//! - `PrefabInstanceList` - Prefab object instances
44//! - `PropList` - Small decorative props
45//!
46//! ## Terrain & Outlines
47//! - `TerrainOutlines` - Terrain boundary definitions
48//! - `NonTerrainOutlines` - Non-terrain area boundaries
49//! - `GoOutlines` - Traversable areas (where units can go)
50//! - `WaterOutlines` - Water body boundaries
51//!
52//! ## Gameplay Elements
53//! - `DeploymentList` - Unit deployment zones
54//! - `CaptureLocationSet` - Groups of capture locations
55//! - `PlayableArea` - Playable map boundaries
56//! - `AIHints` - AI pathfinding and behavior hints
57//!
58//! ## Lighting & Visual Effects
59//! - `PointLightList` - Point light sources
60//! - `SpotLightList` - Spotlight sources
61//! - `LightProbeList` - Global illumination probes
62//! - `ParticleEmitterList` - Particle effect emitters
63//!
64//! ## Vegetation
65//! - `TreeListReferenceList` - Tree placement references
66//! - `GrassListReferenceList` - Grass placement references
67//!
68//! ## Other
69//! - `CameraZones` - Camera constraint zones
70//! - `SoundShapeList` - 3D audio zones
71//! - `CompositeSceneList` - Composite scene references
72//! - `CustomMaterialMeshList` - Custom material meshes
73//! - And 20+ more specialized lists
74//!
75//! # Usage
76//!
77//! ```ignore
78//! use rpfm_lib::files::bmd::Bmd;
79//! use rpfm_lib::files::Decodeable;
80//!
81//! // Decode a BMD file
82//! let bmd = Bmd::decode(&mut reader, &None)?;
83//!
84//! println!("BMD version: {}", bmd.serialise_version());
85//!
86//! // Access building list
87//! for building in bmd.battlefield_building_list().list() {
88//!     println!("Building: {:?}", building.key());
89//! }
90//!
91//! // Export to Terry (CA's editor format)
92//! bmd.export_prefab_to_raw_data("map_name", Some(&vegetation), &output_path)?;
93//! ```
94//!
95//! # Terry Export
96//!
97//! BMD files can be exported to Terry (Creative Assembly's map editor) format using
98//! [`Bmd::export_prefab_to_raw_data()`]. This generates:
99//! - `.terry` project file
100//! - `.layer` scene layer file with entities and associations
101//!
102//! # File Location
103//!
104//! BMD files are typically found in:
105//! ```text
106//! terrain/battles/*.bmd
107//! terrain/tiles/*.bmd
108//! ```
109
110use getset::*;
111use nalgebra::{Matrix3, Rotation3};
112use serde_derive::{Serialize, Deserialize};
113
114use std::collections::HashMap;
115use std::fs::File;
116use std::io::{BufWriter, Write};
117use std::path::Path;
118
119use crate::binary::{ReadBytes, WriteBytes};
120use crate::error::{Result, RLibError};
121use crate::files::{bmd_vegetation::BmdVegetation, Decodeable, DecodeableExtraData, Encodeable, EncodeableExtraData};
122use crate::utils::check_size_mismatch;
123
124use self::battlefield_building_list::BattlefieldBuildingList;
125use self::battlefield_building_list_far::BattlefieldBuildingListFar;
126use self::capture_location_set::CaptureLocationSet;
127use self::common::*;
128use self::ef_line_list::EFLineList;
129use self::go_outlines::GoOutlines;
130use self::non_terrain_outlines::NonTerrainOutlines;
131use self::zones_template_list::ZonesTemplateList;
132use self::prefab_instance_list::PrefabInstanceList;
133use self::bmd_outline_list::BmdOutlineList;
134use self::terrain_outlines::TerrainOutlines;
135use self::lite_building_outlines::LiteBuildingOutlines;
136use self::camera_zones::CameraZones;
137use self::civilian_deployment_list::CivilianDeploymentList;
138use self::civilian_shelter_list::CivilianShelterList;
139use self::prop_list::PropList;
140use self::particle_emitter_list::ParticleEmitterList;
141use self::ai_hints::AIHints;
142use self::light_probe_list::LightProbeList;
143use self::terrain_stencil_triangle_list::TerrainStencilTriangleList;
144use self::point_light_list::PointLightList;
145use self::building_projectile_emitter_list::BuildingProjectileEmitterList;
146use self::playable_area::PlayableArea;
147use self::custom_material_mesh_list::CustomMaterialMeshList;
148use self::terrain_stencil_blend_triangle_list::TerrainStencilBlendTriangleList;
149use self::spot_light_list::SpotLightList;
150use self::sound_shape_list::SoundShapeList;
151use self::composite_scene_list::CompositeSceneList;
152use self::deployment_list::DeploymentList;
153use self::bmd_catchment_area_list::BmdCatchmentAreaList;
154use self::toggleable_buildings_slot_list::ToggleableBuildingsSlotList;
155use self::terrain_decal_list::TerrainDecalList;
156use self::tree_list_reference_list::TreeListReferenceList;
157use self::grass_list_reference_list::GrassListReferenceList;
158use self::water_outlines::WaterOutlines;
159
160/// File extension for Battle Map Definition files.
161///
162/// BMD files use the `.bmd` extension.
163pub const EXTENSIONS: [&str; 1] = [
164    ".bmd",
165];
166
167/// FASTBIN0 file signature.
168///
169/// All BMD files start with this 8-byte signature: `FASTBIN0`
170/// (bytes: `[0x46, 0x41, 0x53, 0x54, 0x42, 0x49, 0x4E, 0x30]`)
171pub const SIGNATURE: &[u8; 8] = &[0x46, 0x41, 0x53, 0x54, 0x42, 0x49, 0x4E, 0x30];
172
173mod battlefield_building_list;
174mod battlefield_building_list_far;
175mod capture_location_set;
176mod ef_line_list;
177mod go_outlines;
178mod non_terrain_outlines;
179mod zones_template_list;
180mod prefab_instance_list;
181mod bmd_outline_list;
182mod terrain_outlines;
183mod lite_building_outlines;
184mod camera_zones;
185mod civilian_deployment_list;
186mod civilian_shelter_list;
187mod prop_list;
188mod particle_emitter_list;
189mod ai_hints;
190mod light_probe_list;
191mod terrain_stencil_triangle_list;
192mod point_light_list;
193mod building_projectile_emitter_list;
194mod playable_area;
195mod custom_material_mesh_list;
196mod terrain_stencil_blend_triangle_list;
197mod spot_light_list;
198mod sound_shape_list;
199mod composite_scene_list;
200mod deployment_list;
201mod bmd_catchment_area_list;
202mod toggleable_buildings_slot_list;
203mod terrain_decal_list;
204mod tree_list_reference_list;
205mod grass_list_reference_list;
206mod water_outlines;
207
208pub mod common;
209mod v23;
210mod v24;
211mod v25;
212mod v26;
213mod v27;
214
215#[cfg(test)] mod bmd_test;
216
217//---------------------------------------------------------------------------//
218//                              Enum & Structs
219//---------------------------------------------------------------------------//
220
221/// Represents a complete Battle Map Definition file decoded in memory.
222///
223/// This struct contains all battle map data including buildings, terrain, lighting,
224/// deployment zones, AI hints, and numerous other gameplay and visual elements.
225///
226/// # Field Categories
227///
228/// ## Core
229/// - `serialise_version`: File format version (23-27)
230///
231/// ## Buildings & Objects
232/// - `battlefield_building_list` - Buildings inside the battlefield area
233/// - `battlefield_building_list_far` - Buildings outside the battlefield area
234/// - `prefab_instance_list`: Prefab object instances
235/// - `prop_list`: Small decorative props
236/// - `composite_scene_list`: Composite scene references
237///
238/// ## Terrain & Boundaries
239/// - `terrain_outlines`: Terrain area boundaries
240/// - `non_terrain_outlines`: Non-terrain area boundaries
241/// - `go_outlines`: Traversable area outlines (where units can go).
242/// - `water_outlines`: Water body boundaries
243/// - `bmd_outline_list`: Additional outline definitions
244/// - `lite_building_outlines`: Simplified building outlines
245/// - `playable_area`: Playable map boundaries
246///
247/// ## Gameplay
248/// - `deployment_list`: Unit deployment zones
249/// - `capture_location_set`: Capture point locations
250/// - `bmd_catchment_area_list`: Catchment area definitions
251/// - `zones_template_list`: Zone template definitions
252/// - `ef_line_list`: Entity Formation line definitions
253/// - `ai_hints`: AI pathfinding and behavior hints
254///
255/// ## Lighting
256/// - `point_light_list`: Point light sources
257/// - `spot_light_list`: Spotlight sources
258/// - `light_probe_list`: Global illumination probes
259///
260/// ## Effects & Audio
261/// - `particle_emitter_list`: Particle effect emitters
262/// - `sound_shape_list`: 3D audio zones
263/// - `building_projectile_emitter_list`: Building-based projectile emitters
264///
265/// ## Vegetation
266/// - `tree_list_reference_list`: Tree placement references
267/// - `grass_list_reference_list`: Grass placement references
268///
269/// ## Advanced Rendering
270/// - `custom_material_mesh_list`: Custom material meshes
271/// - `terrain_stencil_triangle_list`: Terrain stencil geometry
272/// - `terrain_stencil_blend_triangle_list`: Blended stencil geometry
273/// - `terrain_decal_list`: Terrain decal placements
274///
275/// ## Civilians & Sieges
276/// - `civilian_deployment_list`: Civilian unit spawns
277/// - `civilian_shelter_list`: Civilian shelter locations
278/// - `toggleable_buildings_slot_list`: Destructible building slots
279///
280/// ## Cameras
281/// - `camera_zones`: Camera constraint volumes
282///
283/// # Getset
284///
285/// All fields have public getters, mutable getters, and setters generated via `getset`:
286/// ```ignore
287/// let version = bmd.serialise_version();  // Getter
288/// *bmd.serialise_version_mut() = 27;      // Mutable getter
289/// bmd.set_serialise_version(27);          // Setter
290/// ```
291///
292/// # Example
293///
294/// ```ignore
295/// use rpfm_lib::files::bmd::Bmd;
296/// use rpfm_lib::files::Decodeable;
297///
298/// let bmd = Bmd::decode(&mut reader, &None)?;
299///
300/// // Check version
301/// println!("BMD version: {}", bmd.serialise_version());
302///
303/// // Iterate buildings
304/// for building in bmd.battlefield_building_list().list() {
305///     println!("Building key: {:?}", building.key());
306/// }
307///
308/// // Access deployment zones
309/// for zone in bmd.deployment_list().list() {
310///     println!("Deployment zone: {:?}", zone);
311/// }
312/// ```
313#[derive(Default, PartialEq, Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)]
314#[getset(get = "pub", get_mut = "pub", set = "pub")]
315pub struct Bmd {
316    /// File format version number (23-27).
317    serialise_version: u16,
318
319    /// Building instances inside the battlefield area.
320    battlefield_building_list: BattlefieldBuildingList,
321
322    /// Building instances outside the battlefield area.
323    battlefield_building_list_far: BattlefieldBuildingListFar,
324
325    /// Groups of capture locations.
326    capture_location_set: CaptureLocationSet,
327
328    /// Entity Formation line definitions.
329    ef_line_list: EFLineList,
330
331    /// Traversable area outlines (where units can go).
332    go_outlines: GoOutlines,
333
334    /// Non-terrain area boundary outlines.
335    non_terrain_outlines: NonTerrainOutlines,
336
337    /// Zone template definitions.
338    zones_template_list: ZonesTemplateList,
339
340    /// Prefab object instances.
341    prefab_instance_list: PrefabInstanceList,
342
343    /// Additional outline definitions.
344    bmd_outline_list: BmdOutlineList,
345
346    /// Terrain area boundary outlines.
347    terrain_outlines: TerrainOutlines,
348
349    /// Simplified building outline definitions.
350    lite_building_outlines: LiteBuildingOutlines,
351
352    /// Camera constraint volumes.
353    camera_zones: CameraZones,
354
355    /// Civilian unit deployment locations.
356    civilian_deployment_list: CivilianDeploymentList,
357
358    /// Civilian shelter locations.
359    civilian_shelter_list: CivilianShelterList,
360
361    /// Small decorative prop instances.
362    prop_list: PropList,
363
364    /// Particle effect emitter instances.
365    particle_emitter_list: ParticleEmitterList,
366
367    /// AI pathfinding and behavior hints.
368    ai_hints: AIHints,
369
370    /// Global illumination light probes.
371    light_probe_list: LightProbeList,
372
373    /// Terrain stencil triangle geometry.
374    terrain_stencil_triangle_list: TerrainStencilTriangleList,
375
376    /// Point light source instances.
377    point_light_list: PointLightList,
378
379    /// Building-based projectile emitter instances.
380    building_projectile_emitter_list: BuildingProjectileEmitterList,
381
382    /// Playable map area boundaries.
383    playable_area: PlayableArea,
384
385    /// Custom material mesh instances.
386    custom_material_mesh_list: CustomMaterialMeshList,
387
388    /// Blended terrain stencil triangle geometry.
389    terrain_stencil_blend_triangle_list: TerrainStencilBlendTriangleList,
390
391    /// Spotlight source instances.
392    spot_light_list: SpotLightList,
393
394    /// 3D audio zone definitions.
395    sound_shape_list: SoundShapeList,
396
397    /// Composite scene references.
398    composite_scene_list: CompositeSceneList,
399
400    /// Unit deployment zone definitions.
401    deployment_list: DeploymentList,
402
403    /// Catchment area definitions.
404    bmd_catchment_area_list: BmdCatchmentAreaList,
405
406    /// Destructible/toggleable building slot definitions.
407    toggleable_buildings_slot_list: ToggleableBuildingsSlotList,
408
409    /// Terrain decal placements.
410    terrain_decal_list: TerrainDecalList,
411
412    /// Tree placement references (links to vegetation files).
413    tree_list_reference_list: TreeListReferenceList,
414
415    /// Grass placement references (links to vegetation files).
416    grass_list_reference_list: GrassListReferenceList,
417
418    /// Water body boundary outlines.
419    water_outlines: WaterOutlines,
420}
421
422//---------------------------------------------------------------------------//
423//                           Implementation of Bmd
424//---------------------------------------------------------------------------//
425
426/// Trait for converting BMD data structures to Terry `.layer` XML format.
427///
428/// This trait is implemented by all BMD data list types to enable export to
429/// Creative Assembly's Terry map editor format. Each implementation converts
430/// its data to XML entity definitions that can be imported into Terry.
431pub trait ToLayer {
432    /// Converts this data structure to Terry `.layer` XML entity definitions.
433    ///
434    /// # Parameters
435    ///
436    /// - `parent`: Reference to the parent [`Bmd`] for accessing cross-referenced data
437    ///
438    /// # Returns
439    ///
440    /// - `Ok(String)`: XML entity definitions for this data list
441    /// - `Err(_)`: Conversion error
442    ///
443    /// # Default Implementation
444    ///
445    /// Returns an empty string (no entities exported).
446    fn to_layer(&self, _parent: &Bmd) -> Result<String> {
447        Ok(String::new())
448    }
449}
450
451impl Decodeable for Bmd {
452
453    fn decode<R: ReadBytes>(data: &mut R, extra_data: &Option<DecodeableExtraData>) -> Result<Self> {
454        let signature_bytes = data.read_slice(8, false)?;
455        if signature_bytes.as_slice() != SIGNATURE {
456            return Err(RLibError::DecodingFastBinUnsupportedSignature(signature_bytes));
457        }
458
459        let mut fastbin = Self::default();
460        fastbin.serialise_version = data.read_u16()?;
461
462        match fastbin.serialise_version {
463            23 => fastbin.read_v23(data, extra_data)?,
464            24 => fastbin.read_v24(data, extra_data)?,
465            25 => fastbin.read_v25(data, extra_data)?,
466            26 => fastbin.read_v26(data, extra_data)?,
467            27 => fastbin.read_v27(data, extra_data)?,
468            _ => return Err(RLibError::DecodingFastBinUnsupportedVersion(String::from("Bmd"), fastbin.serialise_version)),
469        }
470
471        // If we are not in the last byte, it means we didn't parse the entire file, which means this file is corrupt.
472        check_size_mismatch(data.stream_position()? as usize, data.len()? as usize)?;
473
474        Ok(fastbin)
475    }
476}
477
478impl Encodeable for Bmd {
479
480    fn encode<W: WriteBytes>(&mut self, buffer: &mut W, extra_data: &Option<EncodeableExtraData>) -> Result<()> {
481        buffer.write_all(SIGNATURE)?;
482        buffer.write_u16(self.serialise_version)?;
483
484        match self.serialise_version {
485            23 => self.write_v23(buffer, extra_data)?,
486            24 => self.write_v24(buffer, extra_data)?,
487            25 => self.write_v25(buffer, extra_data)?,
488            26 => self.write_v26(buffer, extra_data)?,
489            27 => self.write_v27(buffer, extra_data)?,
490            _ => return Err(RLibError::EncodingFastBinUnsupportedVersion(String::from("Bmd"), self.serialise_version)),
491        }
492
493        Ok(())
494    }
495}
496
497impl Bmd {
498    /// Exports this BMD to Terry (Creative Assembly's editor) format.
499    ///
500    /// Generates two files for use in Terry:
501    /// - `.terry` - Project file defining the prefab structure
502    /// - `.layer` - Scene layer with entities, associations, and vegetation
503    ///
504    /// # Parameters
505    ///
506    /// - `name`: Base name for output files (e.g., "siege_map")
507    /// - `vegetation`: Optional vegetation data from BMD vegetation file
508    /// - `output_path`: Directory where files will be created
509    ///
510    /// # Returns
511    ///
512    /// - `Ok(())`: Successfully exported files
513    /// - `Err(_)`: I/O error or conversion error
514    ///
515    /// # Generated Files
516    ///
517    /// - `{name}.terry` - Project file with scene hierarchy
518    /// - `{name}.187abf10b8b9a13.layer` - Layer file with entities
519    ///
520    /// # Entity Associations
521    ///
522    /// The layer file includes two association types:
523    /// - **Logical**: Parent-child grouping relationships in Terry's UI
524    /// - **Transform**: Spatial/hierarchical relationships
525    ///
526    /// # Example
527    ///
528    /// ```ignore
529    /// use rpfm_lib::files::bmd::Bmd;
530    /// use rpfm_lib::files::bmd_vegetation::BmdVegetation;
531    /// use std::path::Path;
532    ///
533    /// let bmd = Bmd::decode(&mut reader, &None)?;
534    /// let vegetation = BmdVegetation::decode(&mut veg_reader, &None)?;
535    ///
536    /// bmd.export_prefab_to_raw_data(
537    ///     "my_battle_map",
538    ///     Some(&vegetation),
539    ///     Path::new("output/")
540    /// )?;
541    /// ```
542    ///
543    /// # Note
544    ///
545    /// Not all BMD data lists are currently exported. Some are commented out
546    /// in the implementation and will be added as export support is completed.
547    pub fn export_prefab_to_raw_data(&self, name: &str, vegetation: Option<&BmdVegetation>, output_path: &Path) -> Result<()> {
548
549        // We need to generate two files:
550        // - .terry: The project file with just one layer.
551        // - .layer: The layer file with the contents of the bmd and bmd_vegetation.
552        let terry_path = output_path.join(format!("{name}.terry"));
553        let layer_path = output_path.join(format!("{name}.187abf10b8b9a13.layer"));
554
555        let terry_data = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
556<project version=\"27\" id=\"187abf10b7296f5\">
557  <pc type=\"QTU::ProjectPrefab\">
558    <data database=\"battle\" is_skybox=\"0\"/>
559  </pc>
560  <pc type=\"QTU::Scene\">
561    <data version=\"41\">
562      <entity id=\"187abf10b8b9a13\" name=\"Default\">
563        <ECFileLayer export=\"true\" bmd_export_type=\"\"/>
564        <ECFileLayer export=\"true\" bmd_export_type=\"\"/>
565      </entity>
566    </data>
567  </pc>
568  <pc type=\"QTU::Terrain\"/>
569</project>".to_string();
570
571        let mut terry_file = BufWriter::new(File::create(terry_path)?);
572        terry_file.write_all(terry_data.as_bytes())?;
573
574        // Pre-calculate the associations section.
575        let assoc_logical = self.logical_associations();
576        let assoc_transform = self.trasnform_associations();
577
578        // Now the layer.
579        let mut layer_data = String::new();
580
581        layer_data.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>
582<layer version=\"41\">
583    <entities>"
584        );
585
586        layer_data.push_str(&self.battlefield_building_list().to_layer(self)?);
587        layer_data.push_str(&self.battlefield_building_list_far().to_layer(self)?);
588        layer_data.push_str(&self.capture_location_set().to_layer(self)?);
589        //layer_data.push_str(&self.ef_line_list().to_layer(self)?);
590        //layer_data.push_str(&self.go_outlines().to_layer(self)?);
591        //layer_data.push_str(&self.non_terrain_outlines().to_layer(self)?);
592        //layer_data.push_str(&self.zones_template_list().to_layer(self)?);
593        layer_data.push_str(&self.prefab_instance_list().to_layer(self)?);
594        //layer_data.push_str(&self.bmd_outline_list().to_layer(self)?);
595        //layer_data.push_str(&self.terrain_outlines().to_layer(self)?);
596        //layer_data.push_str(&self.lite_building_outlines().to_layer(self)?);
597        //layer_data.push_str(&self.camera_zones().to_layer(self)?);
598        //layer_data.push_str(&self.civilian_deployment_list().to_layer(self)?);
599        //layer_data.push_str(&self.civilian_shelter_list().to_layer(self)?);
600        //layer_data.push_str(&self.prop_list().to_layer(self)?);
601        //layer_data.push_str(&self.particle_emitter_list().to_layer(self)?);
602        //layer_data.push_str(&self.ai_hints().to_layer(self)?);
603        //layer_data.push_str(&self.light_probe_list().to_layer(self)?);
604        //layer_data.push_str(&self.terrain_stencil_triangle_list().to_layer(self)?);
605        //layer_data.push_str(&self.point_light_list().to_layer(self)?);
606        //layer_data.push_str(&self.building_projectile_emitter_list().to_layer(self)?);
607        //layer_data.push_str(&self.playable_area().to_layer(self)?);
608        //layer_data.push_str(&self.custom_material_mesh_list().to_layer(self)?);
609        //layer_data.push_str(&self.terrain_stencil_blend_triangle_list().to_layer(self)?);
610        //layer_data.push_str(&self.spot_light_list().to_layer(self)?);
611        //layer_data.push_str(&self.sound_shape_list().to_layer(self)?);
612        //layer_data.push_str(&self.composite_scene_list().to_layer(self)?);
613        //layer_data.push_str(&self.deployment_list().to_layer(self)?);
614        //layer_data.push_str(&self.bmd_catchment_area_list().to_layer(self)?);
615        //layer_data.push_str(&self.toggleable_buildings_slot_list().to_layer(self)?);
616        //layer_data.push_str(&self.terrain_decal_list().to_layer(self)?);
617        //layer_data.push_str(&self.tree_list_reference_list().to_layer(self)?);
618        //layer_data.push_str(&self.grass_list_reference_list().to_layer(self)?);
619        //layer_data.push_str(&self.water_outlines().to_layer(self)?);
620
621        // Vegetation items are entities in the layer too.
622        if let Some(vegetation) = vegetation {
623            layer_data.push_str(&vegetation.to_layer(self)?);
624        }
625
626        layer_data.push_str("
627    </entities>
628    <associations>");
629
630        if assoc_logical.is_empty() {
631            layer_data.push_str("
632        <Logical/>");
633        } else {
634            layer_data.push_str("
635        <Logical>");
636
637            for (key, values) in &assoc_logical {
638                layer_data.push_str(&format!("
639            <from id=\"{key}\">"
640                ));
641
642                for value in values {
643                    layer_data.push_str(&format!("
644                <to id=\"{value}\"/>"
645                    ));
646                }
647
648                layer_data.push_str("
649            </from>");
650            }
651
652            layer_data.push_str("
653        </Logical>");
654        }
655
656        if assoc_transform.is_empty() {
657            layer_data.push_str("
658        <Transform/>");
659        } else {
660
661            layer_data.push_str("
662        <Transform>");
663
664            for (key, values) in &assoc_transform {
665                layer_data.push_str(&format!("
666            <from id=\"{key}\">"
667                ));
668
669                for value in values {
670                    layer_data.push_str(&format!("
671                <to id=\"{value}\"/>"
672                    ));
673                }
674
675                layer_data.push_str("
676            </from>");
677            }
678
679            layer_data.push_str("
680        </Transform>");
681        }
682
683        layer_data.push_str("
684    </associations>
685</layer>
686        ");
687
688        let mut layer_file = BufWriter::new(File::create(layer_path)?);
689        layer_file.write_all(layer_data.as_bytes())?;
690
691        Ok(())
692    }
693
694    /// Returns logical entity associations for Terry export.
695    ///
696    /// Logical associations define parent-child grouping relationships for
697    /// organizing entities in Terry's UI hierarchy.
698    ///
699    /// # Returns
700    ///
701    /// Map of parent entity IDs to their logically grouped child entity IDs.
702    ///
703    /// # Note
704    ///
705    /// Currently returns an empty map. Will be populated as association
706    /// logic is implemented.
707    pub fn logical_associations(&self) -> HashMap<String, Vec<String>> {
708        HashMap::new()
709    }
710
711    /// Returns transform (spatial hierarchy) entity associations for Terry export.
712    ///
713    /// Transform associations define parent-child spatial relationships between
714    /// entities (e.g., props attached to buildings, lights attached to structures).
715    ///
716    /// # Returns
717    ///
718    /// Map of parent entity IDs to their child entity IDs.
719    ///
720    /// # Note
721    ///
722    /// Currently returns an empty map. Will be populated as association
723    /// logic is implemented.
724    pub fn trasnform_associations(&self) -> HashMap<String, Vec<String>> {
725        HashMap::new()
726    }
727}