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}