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}