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