vmf_forge/vmf/
world.rs

1//! This module provides structures for representing the world block in a VMF file, which contains world geometry, hidden entities, and groups.
2
3use indexmap::IndexMap;
4use serde::{Deserialize, Serialize};
5
6use super::common::Editor;
7use crate::utils::{get_key, parse_hs_key, To01String};
8use crate::{
9    errors::{VmfError, VmfResult},
10    VmfBlock, VmfSerializable,
11};
12
13/// Represents the world block in a VMF file.
14#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
15pub struct World {
16    /// The key-value pairs associated with the world.
17    pub key_values: IndexMap<String, String>,
18    /// The list of solids that make up the world geometry.
19    pub solids: Vec<Solid>,
20    /// The list of hidden solids in the world.
21    pub hidden: Vec<Solid>,
22    /// The groups present in the world, if any.
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub group: Option<Group>,
25}
26
27impl TryFrom<VmfBlock> for World {
28    type Error = VmfError;
29
30    fn try_from(block: VmfBlock) -> VmfResult<Self> {
31        let mut world = World {
32            key_values: block.key_values,
33            ..Default::default()
34        };
35
36        for inner_block in block.blocks {
37            match inner_block.name.as_str() {
38                "solid" => world.solids.push(Solid::try_from(inner_block)?),
39                "group" => world.group = Group::try_from(inner_block).ok(),
40                "hidden" => {
41                    if let Some(hidden_block) = inner_block.blocks.first() {
42                        world.hidden.push(Solid::try_from(hidden_block.to_owned())?);
43                    }
44                }
45                _ => {
46                    // The `world` block does not support other types of blocks (except `hidden`, `group` and `solid`)
47                    #[cfg(feature = "debug_assert_info")]
48                    debug_assert!(false, "Unexpected block name: {}", inner_block.name);
49                }
50            };
51        }
52
53        Ok(world)
54    }
55}
56
57impl From<World> for VmfBlock {
58    fn from(val: World) -> Self {
59        let mut blocks = Vec::new();
60
61        // Add solids
62        for solid in val.solids {
63            blocks.push(solid.into());
64        }
65
66        // Add hidden solids
67        for hidden_solid in val.hidden {
68            blocks.push(VmfBlock {
69                name: "hidden".to_string(),
70                key_values: IndexMap::new(),
71                blocks: vec![hidden_solid.into()],
72            });
73        }
74
75        // Add groups
76        if let Some(group) = val.group {
77            blocks.push(group.into());
78        }
79
80        VmfBlock {
81            name: "world".to_string(),
82            key_values: val.key_values,
83            blocks,
84        }
85    }
86}
87
88impl VmfSerializable for World {
89    fn to_vmf_string(&self, indent_level: usize) -> String {
90        let indent = "\t".repeat(indent_level);
91        let mut output = String::with_capacity(2048);
92
93        output.push_str(&format!("{0}world\n{0}{{\n", indent));
94
95        // Adds key_values of the main block
96        for (key, value) in &self.key_values {
97            output.push_str(&format!("{}\t\"{}\" \"{}\"\n", indent, key, value));
98        }
99
100        // Solids Block
101        if !self.solids.is_empty() {
102            for solid in &self.solids {
103                output.push_str(&solid.to_vmf_string(indent_level + 1));
104            }
105        }
106
107        // Hidden Solids Block
108        if !self.hidden.is_empty() {
109            output.push_str(&format!("{0}\tHidden\n{0}\t{{\n", indent));
110            for solid in &self.hidden {
111                output.push_str(&solid.to_vmf_string(indent_level + 2));
112            }
113            output.push_str(&format!("{}\t}}\n", indent));
114        }
115
116        // Group Block
117        if let Some(group) = &self.group {
118            output.push_str(&group.to_vmf_string(indent_level + 1));
119        }
120
121        output.push_str(&format!("{}}}\n", indent));
122        output
123    }
124}
125
126/// Represents a solid object in the VMF world.
127#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
128pub struct Solid {
129    /// The unique ID of the solid.
130    pub id: u64,
131    /// The sides of the solid.
132    pub sides: Vec<Side>,
133    /// The editor data for the solid.
134    pub editor: Editor,
135}
136
137impl TryFrom<VmfBlock> for Solid {
138    type Error = VmfError;
139
140    fn try_from(block: VmfBlock) -> VmfResult<Self> {
141        let mut solid = Solid {
142            id: parse_hs_key!(&block.key_values, "id", u64)?,
143            sides: Vec::with_capacity(4),
144            ..Default::default()
145        };
146
147        for inner_block in block.blocks {
148            match inner_block.name.as_str() {
149                "side" => solid.sides.push(Side::try_from(inner_block)?),
150                "editor" => solid.editor = Editor::try_from(inner_block)?,
151                _ => {
152                    #[cfg(feature = "debug_assert_info")]
153                    debug_assert!(false, "Unexpected block name: {}", inner_block.name);
154                }
155            }
156        }
157
158        Ok(solid)
159    }
160}
161
162impl From<Solid> for VmfBlock {
163    fn from(val: Solid) -> Self {
164        let mut blocks = Vec::new();
165
166        // Adds sides
167        for side in val.sides {
168            blocks.push(side.into());
169        }
170
171        // Adds editor
172        blocks.push(val.editor.into());
173
174        VmfBlock {
175            name: "solid".to_string(),
176            key_values: {
177                let mut key_values = IndexMap::new();
178                key_values.insert("id".to_string(), val.id.to_string());
179                key_values
180            },
181            blocks,
182        }
183    }
184}
185
186impl VmfSerializable for Solid {
187    fn to_vmf_string(&self, indent_level: usize) -> String {
188        let indent = "\t".repeat(indent_level);
189        let mut output = String::with_capacity(256);
190
191        // Start of solid block
192        output.push_str(&format!("{0}solid\n{0}{{\n", indent));
193        output.push_str(&format!("{}\t\"id\" \"{}\"\n", indent, self.id));
194
195        // Sides
196        for side in &self.sides {
197            output.push_str(&side.to_vmf_string(indent_level + 1));
198        }
199
200        // Editor block
201        output.push_str(&self.editor.to_vmf_string(indent_level + 1));
202
203        output.push_str(&format!("{}}}\n", indent));
204
205        output
206    }
207}
208
209/// Represents a side of a solid object in the VMF world.
210#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
211pub struct Side {
212    /// The unique ID of the side.
213    pub id: u32,
214    /// The plane equation of the side.
215    pub plane: String,
216    /// The material used on the side.
217    pub material: String,
218    /// The U axis of the texture coordinates.
219    pub u_axis: String,
220    /// The V axis of the texture coordinates.
221    pub v_axis: String,
222    /// The rotation of the texture.
223    #[serde(default, skip_serializing_if = "Option::is_none")]
224    pub rotation: Option<f32>,
225    /// The scale of the lightmap.
226    pub lightmap_scale: u16,
227    /// The smoothing groups that this side belongs to.
228    pub smoothing_groups: i32,
229    /// flags
230    #[serde(default, skip_serializing_if = "Option::is_none")]
231    pub flags: Option<u32>,
232    /// The displacement info of the side, if any.
233    #[serde(default, skip_serializing_if = "Option::is_none")]
234    pub dispinfo: Option<DispInfo>,
235}
236
237impl TryFrom<VmfBlock> for Side {
238    type Error = VmfError;
239
240    fn try_from(block: VmfBlock) -> VmfResult<Self> {
241        let kv = &block.key_values;
242        let dispinfo_block = block.blocks.iter().find(|b| b.name == "dispinfo");
243
244        Ok(Side {
245            id: parse_hs_key!(kv, "id", u32)?,
246            plane: get_key!(kv, "plane")?.to_owned(),
247            material: get_key!(kv, "material")?.to_owned(),
248            u_axis: get_key!(kv, "uaxis")?.to_owned(),
249            v_axis: get_key!(kv, "vaxis")?.to_owned(),
250            rotation: get_key!(kv, "rotation", "_".into()).parse().ok(),
251            lightmap_scale: parse_hs_key!(kv, "lightmapscale", u16)?,
252            smoothing_groups: parse_hs_key!(kv, "smoothing_groups", i32)?,
253            flags: get_key!(kv, "flags", "_".into()).parse().ok(),
254            dispinfo: match dispinfo_block {
255                Some(block) => Some(DispInfo::try_from(block.clone())?), // todo: clone :<
256                None => None,
257            },
258        })
259    }
260}
261
262impl From<Side> for VmfBlock {
263    fn from(val: Side) -> Self {
264        let mut key_values = IndexMap::new();
265        key_values.insert("id".to_string(), val.id.to_string());
266        key_values.insert("plane".to_string(), val.plane);
267        key_values.insert("material".to_string(), val.material);
268        key_values.insert("uaxis".to_string(), val.u_axis);
269        key_values.insert("vaxis".to_string(), val.v_axis);
270        if let Some(rotation) = val.rotation {
271            key_values.insert("rotation".to_string(), rotation.to_string());
272        }
273        key_values.insert("lightmapscale".to_string(), val.lightmap_scale.to_string());
274        key_values.insert(
275            "smoothing_groups".to_string(),
276            val.smoothing_groups.to_string(),
277        );
278        if let Some(flags) = val.flags {
279            key_values.insert("flags".to_string(), flags.to_string());
280        }
281
282        VmfBlock {
283            name: "side".to_string(),
284            key_values,
285            blocks: Vec::new(),
286        }
287    }
288}
289
290impl VmfSerializable for Side {
291    fn to_vmf_string(&self, indent_level: usize) -> String {
292        let indent = "\t".repeat(indent_level);
293        let mut output = String::with_capacity(256);
294
295        // Start of Side block
296        output.push_str(&format!("{0}side\n{0}{{\n", indent));
297
298        // Writes all key-value pairs with appropriate indentation
299        output.push_str(&format!("{}\t\"id\" \"{}\"\n", indent, self.id));
300        output.push_str(&format!("{}\t\"plane\" \"{}\"\n", indent, self.plane));
301        output.push_str(&format!("{}\t\"material\" \"{}\"\n", indent, self.material));
302        output.push_str(&format!("{}\t\"uaxis\" \"{}\"\n", indent, self.u_axis));
303        output.push_str(&format!("{}\t\"vaxis\" \"{}\"\n", indent, self.v_axis));
304
305        if let Some(rotation) = self.rotation {
306            output.push_str(&format!("{}\t\"rotation\" \"{}\"\n", indent, rotation));
307        }
308
309        output.push_str(&format!(
310            "{}\t\"lightmapscale\" \"{}\"\n",
311            indent, self.lightmap_scale
312        ));
313        output.push_str(&format!(
314            "{}\t\"smoothing_groups\" \"{}\"\n",
315            indent, self.smoothing_groups
316        ));
317
318        // Adds the flag if it exists
319        if let Some(flags) = self.flags {
320            output.push_str(&format!("{}\t\"flags\" \"{}\"\n", indent, flags));
321        }
322
323        if let Some(dispinfo) = &self.dispinfo {
324            output.push_str(&dispinfo.to_vmf_string(indent_level + 1));
325        }
326
327        // End of Side block
328        output.push_str(&format!("{0}}}\n", indent));
329
330        output
331    }
332}
333
334/// Looks for a block with the specified name in a vector of `VmfBlock`s.
335///
336/// # Arguments
337///
338/// * `blocks` - A slice of `VmfBlock`s to search in.
339/// * `name` - The name of the block to search for.
340///
341/// # Returns
342///
343/// A `Result` containing a reference to the first `VmfBlock` with the specified name, or a `VmfError` if no such block is found.
344macro_rules! find_block {
345    ($blocks:expr, $name:expr) => {
346        $blocks.iter().find(|b| b.name == $name).ok_or_else(|| {
347            VmfError::InvalidFormat(format!("Missing {} block in dispinfo", $name))
348        })?
349    };
350}
351
352/// Represents the displacement information for a side.
353#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
354pub struct DispInfo {
355    /// The power of the displacement map (2, 3, or 4).
356    pub power: u8,
357    /// The starting position of the displacement.
358    pub start_position: String,
359    /// Flags for the displacement.
360    #[serde(default, skip_serializing_if = "Option::is_none")]
361    pub flags: Option<u32>,
362    /// The elevation of the displacement.
363    pub elevation: f32,
364    /// Whether the displacement is subdivided.
365    pub subdiv: bool,
366    /// The normals for each vertex in the displacement.
367    pub normals: DispRows,
368    /// The distances for each vertex in the displacement.
369    pub distances: DispRows,
370    /// The offsets for each vertex in the displacement.
371    pub offsets: DispRows,
372    /// The offset normals for each vertex in the displacement.
373    pub offset_normals: DispRows,
374    /// The alpha values for each vertex in the displacement.
375    pub alphas: DispRows,
376    /// The triangle tags for the displacement.
377    pub triangle_tags: DispRows,
378    /// The allowed vertices for the displacement.
379    pub allowed_verts: IndexMap<String, Vec<i32>>,
380}
381
382impl TryFrom<VmfBlock> for DispInfo {
383    type Error = VmfError;
384
385    fn try_from(block: VmfBlock) -> VmfResult<Self> {
386        let normals_block = find_block!(block.blocks, "normals");
387        let distances_block = find_block!(block.blocks, "distances");
388        let alphas_block = find_block!(block.blocks, "alphas");
389        let triangle_tags_block = find_block!(block.blocks, "triangle_tags");
390        let allowed_verts_block = find_block!(block.blocks, "allowed_verts");
391
392        // These blocks may not be present in the decompiled vmf. Why?
393        let offsets = block.blocks.iter()
394            .find(|b| b.name == "offsets")
395            .map_or_else(
396                || Ok(DispRows::default()),
397                |b| DispRows::try_from(b.clone())
398            )?;
399
400        let offset_normals = block.blocks.iter()
401            .find(|b| b.name == "offset_normals")
402            .map_or_else(
403                || Ok(DispRows::default()),
404                |b| DispRows::try_from(b.clone())
405            )?;
406
407
408        let kv = &block.key_values;
409        Ok(DispInfo {
410            power: parse_hs_key!(kv, "power", u8)?,
411            start_position: get_key!(kv, "startposition")?.to_string(),
412            flags: get_key!(kv, "flags", "_".into()).parse().ok(),
413            elevation: get_key!(kv, "elevation")?.parse()?,
414            subdiv: get_key!(kv, "subdiv")? == "1",
415            normals: DispRows::try_from(normals_block.clone())?,
416            distances: DispRows::try_from(distances_block.clone())?,
417            offsets,
418            offset_normals,
419            alphas: DispRows::try_from(alphas_block.clone())?,
420            triangle_tags: DispRows::try_from(triangle_tags_block.clone())?,
421            allowed_verts: DispInfo::parse_allowed_verts(allowed_verts_block)?,
422        })
423    }
424}
425
426impl From<DispInfo> for VmfBlock {
427    fn from(val: DispInfo) -> Self {
428        let blocks = vec![
429            val.normals.into_vmf_block("normals"),
430            val.distances.into_vmf_block("distances"),
431            val.offsets.into_vmf_block("offsets"),
432            val.offset_normals.into_vmf_block("offset_normals"),
433            val.alphas.into_vmf_block("alphas"),
434            val.triangle_tags.into_vmf_block("triangle_tags"),
435            DispInfo::allowed_verts_into_vmf_block(val.allowed_verts),
436        ];
437
438        let mut key_values = IndexMap::new();
439        key_values.insert("power".to_string(), val.power.to_string());
440        key_values.insert("startposition".to_string(), val.start_position);
441        key_values.insert("elevation".to_string(), val.elevation.to_string());
442        key_values.insert("subdiv".to_string(), val.subdiv.to_01_string());
443
444        if let Some(flags) = val.flags {
445            key_values.insert("flags".to_string(), flags.to_string());
446        }
447
448        VmfBlock {
449            name: "dispinfo".to_string(),
450            key_values,
451            blocks,
452        }
453    }
454}
455
456impl VmfSerializable for DispInfo {
457    fn to_vmf_string(&self, indent_level: usize) -> String {
458        let indent = "\t".repeat(indent_level);
459        let mut output = String::with_capacity(256);
460
461        output.push_str(&format!("{}dispinfo\n", indent));
462        output.push_str(&format!("{}{{\n", indent));
463        output.push_str(&format!("{}\t\"power\" \"{}\"\n", indent, self.power));
464        output.push_str(&format!(
465            "{}\t\"startposition\" \"{}\"\n",
466            indent, self.start_position
467        ));
468
469        // Adds the flag if it exists
470        if let Some(flags) = self.flags {
471            output.push_str(&format!("{}\t\"flags\" \"{}\"\n", indent, flags));
472        }
473
474        output.push_str(&format!(
475            "{}\t\"elevation\" \"{}\"\n",
476            indent, self.elevation
477        ));
478        output.push_str(&format!(
479            "{}\t\"subdiv\" \"{}\"\n",
480            indent,
481            self.subdiv.to_01_string()
482        ));
483        output.push_str(&self.normals.to_vmf_string(indent_level + 1, "normals"));
484        output.push_str(&self.distances.to_vmf_string(indent_level + 1, "distances"));
485        output.push_str(&self.offsets.to_vmf_string(indent_level + 1, "offsets"));
486        output.push_str(
487            &self
488                .offset_normals
489                .to_vmf_string(indent_level + 1, "offset_normals"),
490        );
491        output.push_str(&self.alphas.to_vmf_string(indent_level + 1, "alphas"));
492        output.push_str(
493            &self
494                .triangle_tags
495                .to_vmf_string(indent_level + 1, "triangle_tags"),
496        );
497        output.push_str(&Self::allowed_verts_to_vmf_string(
498            &self.allowed_verts,
499            indent_level + 1,
500        ));
501        output.push_str(&format!("{}}}\n", indent));
502
503        output
504    }
505}
506
507impl DispInfo {
508    /// Parses the allowed vertices from a `VmfBlock`.
509    ///
510    /// # Arguments
511    ///
512    /// * `block` - The `VmfBlock` containing the allowed vertices data.
513    ///
514    /// # Returns
515    ///
516    /// A `Result` containing an `IndexMap` of allowed vertices, or a `VmfError` if parsing fails.
517    fn parse_allowed_verts(block: &VmfBlock) -> VmfResult<IndexMap<String, Vec<i32>>> {
518        let mut allowed_verts = IndexMap::new();
519        for (key, value) in &block.key_values {
520            let verts: VmfResult<Vec<i32>> = value
521                .split_whitespace()
522                .map(|s| {
523                    s.parse::<i32>()
524                        .map_err(|e| VmfError::ParseInt(e, s.to_string()))
525                })
526                .collect();
527            allowed_verts.insert(key.clone(), verts?);
528        }
529        Ok(allowed_verts)
530    }
531
532    /// Converts the allowed vertices data into a `VmfBlock`.
533    ///
534    /// # Arguments
535    ///
536    /// * `allowed_verts` - The `IndexMap` containing the allowed vertices data.
537    ///
538    /// # Returns
539    ///
540    /// A `VmfBlock` representing the allowed vertices data.
541    fn allowed_verts_into_vmf_block(allowed_verts: IndexMap<String, Vec<i32>>) -> VmfBlock {
542        let mut key_values = IndexMap::new();
543        for (key, values) in allowed_verts {
544            key_values.insert(
545                key,
546                values
547                    .iter()
548                    .map(|v| v.to_string())
549                    .collect::<Vec<String>>()
550                    .join(" "),
551            );
552        }
553
554        VmfBlock {
555            name: "allowed_verts".to_string(),
556            key_values,
557            blocks: Vec::new(),
558        }
559    }
560
561    /// Converts the allowed vertices data into a string representation.
562    ///
563    /// # Arguments
564    ///
565    /// * `allowed_verts` - A reference to an `IndexMap` containing the allowed vertices data.
566    /// * `indent_level` - The indentation level for formatting.
567    ///
568    /// # Returns
569    ///
570    /// A string representation of the allowed vertices data.
571    fn allowed_verts_to_vmf_string(
572        allowed_verts: &IndexMap<String, Vec<i32>>,
573        indent_level: usize,
574    ) -> String {
575        let indent = "\t".repeat(indent_level);
576        let mut output = String::new();
577
578        output.push_str(&format!("{}allowed_verts\n", indent));
579        output.push_str(&format!("{}{{\n", indent));
580        for (key, values) in allowed_verts {
581            output.push_str(&format!(
582                "{}\t\"{}\" \"{}\"\n",
583                indent,
584                key,
585                values
586                    .iter()
587                    .map(|v| v.to_string())
588                    .collect::<Vec<String>>()
589                    .join(" ")
590            ));
591        }
592        output.push_str(&format!("{}}}\n", indent));
593
594        output
595    }
596}
597
598/// Represents rows of data for displacement information, such as normals, distances, offsets, etc.
599#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
600pub struct DispRows {
601    /// The rows of data, each represented as a string.
602    pub rows: Vec<String>,
603}
604
605impl TryFrom<VmfBlock> for DispRows {
606    type Error = VmfError;
607
608    fn try_from(block: VmfBlock) -> VmfResult<Self> {
609        let mut rows = Vec::with_capacity(block.key_values.len());
610        for (key, value) in block.key_values {
611            if let Some(stripped_idx) = key.strip_prefix("row") {
612                let index = stripped_idx
613                    .parse::<usize>()
614                    .map_err(|e| VmfError::ParseInt(e, key.to_string()))?;
615                if index >= rows.len() {
616                    rows.resize(index + 1, String::new());
617                }
618                rows[index] = value;
619            }
620        }
621        Ok(DispRows { rows })
622    }
623}
624
625impl DispRows {
626    /// Converts the `DispRows` data into a `VmfBlock` with the specified name.
627    ///
628    /// # Arguments
629    ///
630    /// * `self` - The `DispRows` instance to convert.
631    /// * `name` - The name of the block.
632    ///
633    /// # Returns
634    ///
635    /// A `VmfBlock` representing the `DispRows` data.
636    fn into_vmf_block(self, name: &str) -> VmfBlock {
637        let mut key_values = IndexMap::new();
638        for (i, row) in self.rows.into_iter().enumerate() {
639            key_values.insert(format!("row{}", i), row);
640        }
641
642        VmfBlock {
643            name: name.to_string(),
644            key_values,
645            blocks: Vec::new(),
646        }
647    }
648
649    /// Converts the `DispRows` data into a string representation with the specified name and indentation level.
650    ///
651    /// # Arguments
652    ///
653    /// * `self` - A reference to the `DispRows` instance.
654    /// * `indent_level` - The indentation level for formatting.
655    /// * `name` - The name of the block.
656    ///
657    /// # Returns
658    ///
659    /// A string representation of the `DispRows` data.
660    fn to_vmf_string(&self, indent_level: usize, name: &str) -> String {
661        let indent = "\t".repeat(indent_level);
662        let mut output = String::with_capacity(32);
663
664        output.push_str(&format!("{}{}\n", indent, name));
665        output.push_str(&format!("{}{{\n", indent));
666        for (i, row) in self.rows.iter().enumerate() {
667            output.push_str(&format!("{}\t\"row{}\" \"{}\"\n", indent, i, row));
668        }
669        output.push_str(&format!("{}}}\n", indent));
670
671        output
672    }
673}
674
675/// Represents a group in the VMF world.
676#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
677pub struct Group {
678    /// The unique ID of the group.
679    pub id: u32,
680    /// The editor data for the group.
681    pub editor: Editor,
682}
683
684impl TryFrom<VmfBlock> for Group {
685    type Error = VmfError;
686
687    fn try_from(block: VmfBlock) -> VmfResult<Self> {
688        let mut editor = None;
689        for inner_block in block.blocks {
690            if inner_block.name.eq_ignore_ascii_case("editor") {
691                editor = Some(Editor::try_from(inner_block)?);
692            }
693        }
694
695        Ok(Self {
696            id: parse_hs_key!(&block.key_values, "id", u32)?,
697            editor: editor.unwrap_or_default(),
698        })
699    }
700}
701
702impl From<Group> for VmfBlock {
703    fn from(val: Group) -> Self {
704        let mut blocks = Vec::with_capacity(2);
705
706        // Adds Editor block
707        blocks.push(val.editor.into());
708
709        VmfBlock {
710            name: "group".to_string(),
711            key_values: {
712                let mut key_values = IndexMap::new();
713                key_values.insert("id".to_string(), val.id.to_string());
714                key_values
715            },
716            blocks,
717        }
718    }
719}
720
721impl VmfSerializable for Group {
722    fn to_vmf_string(&self, indent_level: usize) -> String {
723        let indent = "\t".repeat(indent_level);
724        let mut output = String::with_capacity(64);
725
726        // Writes the main entity block
727        output.push_str(&format!("{0}group\n{0}{{\n", indent));
728        output.push_str(&format!("{}\t\"id\" \"{}\"\n", indent, self.id));
729
730        // Editor block
731        output.push_str(&self.editor.to_vmf_string(indent_level + 1));
732
733        output.push_str(&format!("{}}}\n", indent));
734
735        output
736    }
737}