wow_wmo/
editor.rs

1//! High-level WMO editing API
2//!
3//! This module provides a user-friendly interface for modifying WMO files,
4//! including materials, groups, transformations, and doodad management.
5
6use crate::converter::WmoConverter;
7use crate::error::{Result, WmoError};
8use crate::types::{BoundingBox, Vec3};
9use crate::version::WmoVersion;
10use crate::wmo_group_types::{WmoGroup, WmoGroupHeader};
11use crate::wmo_types::{WmoDoodadDef, WmoDoodadSet, WmoGroupInfo, WmoMaterial, WmoRoot};
12use crate::writer::WmoWriter;
13
14// Use WmoGroupFlags from wmo_group_types since that's where WmoGroupHeader uses it
15use crate::wmo_group_types::WmoGroupFlags;
16
17/// WMO editor for modifying WMO files
18pub struct WmoEditor {
19    /// Root WMO data
20    root: WmoRoot,
21
22    /// Group WMO data
23    groups: Vec<WmoGroup>,
24
25    /// Modified flag for root
26    root_modified: bool,
27
28    /// Modified flags for groups
29    group_modified: Vec<bool>,
30
31    /// Original version
32    original_version: WmoVersion,
33}
34
35impl WmoEditor {
36    /// Create a new WMO editor from a root WMO file
37    pub fn new(root: WmoRoot) -> Self {
38        let original_version = root.version;
39        let group_count = root.groups.len();
40
41        Self {
42            root,
43            groups: Vec::with_capacity(group_count),
44            root_modified: false,
45            group_modified: vec![false; group_count],
46            original_version,
47        }
48    }
49
50    /// Add a group to the editor
51    pub fn add_group(&mut self, group: WmoGroup) -> Result<()> {
52        // Verify group index
53        let group_index = group.header.group_index as usize;
54
55        if group_index >= self.root.groups.len() {
56            return Err(WmoError::InvalidReference {
57                field: "group_index".to_string(),
58                value: group_index as u32,
59                max: self.root.groups.len() as u32 - 1,
60            });
61        }
62
63        // Ensure groups vector has enough capacity
64        if self.groups.len() <= group_index {
65            self.groups.resize_with(group_index + 1, || WmoGroup {
66                header: WmoGroupHeader {
67                    flags: WmoGroupFlags::empty(),
68                    bounding_box: BoundingBox {
69                        min: Vec3 {
70                            x: 0.0,
71                            y: 0.0,
72                            z: 0.0,
73                        },
74                        max: Vec3 {
75                            x: 0.0,
76                            y: 0.0,
77                            z: 0.0,
78                        },
79                    },
80                    name_offset: 0,
81                    group_index: 0,
82                },
83                materials: Vec::new(),
84                vertices: Vec::new(),
85                normals: Vec::new(),
86                tex_coords: Vec::new(),
87                batches: Vec::new(),
88                indices: Vec::new(),
89                vertex_colors: None,
90                bsp_nodes: None,
91                liquid: None,
92                doodad_refs: None,
93            });
94        }
95
96        // Store the group
97        self.groups[group_index] = group;
98        self.group_modified[group_index] = true;
99
100        Ok(())
101    }
102
103    /// Get a reference to the root WMO
104    pub fn root(&self) -> &WmoRoot {
105        &self.root
106    }
107
108    /// Get a mutable reference to the root WMO
109    pub fn root_mut(&mut self) -> &mut WmoRoot {
110        self.root_modified = true;
111        &mut self.root
112    }
113
114    /// Get a reference to a specific group
115    pub fn group(&self, index: usize) -> Option<&WmoGroup> {
116        self.groups.get(index)
117    }
118
119    /// Get a mutable reference to a specific group
120    pub fn group_mut(&mut self, index: usize) -> Option<&mut WmoGroup> {
121        if index < self.group_modified.len() {
122            self.group_modified[index] = true;
123        }
124        self.groups.get_mut(index)
125    }
126
127    /// Get the number of groups
128    pub fn group_count(&self) -> usize {
129        self.root.groups.len()
130    }
131
132    /// Check if a specific group has been loaded
133    pub fn is_group_loaded(&self, index: usize) -> bool {
134        index < self.groups.len() && self.group(index).is_some()
135    }
136
137    /// Check if a specific group has been modified
138    pub fn is_group_modified(&self, index: usize) -> bool {
139        index < self.group_modified.len() && self.group_modified[index]
140    }
141
142    /// Check if the root has been modified
143    pub fn is_root_modified(&self) -> bool {
144        self.root_modified
145    }
146
147    /// Convert to a specific version
148    pub fn convert_to_version(&mut self, target_version: WmoVersion) -> Result<()> {
149        // Only convert if necessary
150        if self.root.version == target_version {
151            return Ok(());
152        }
153
154        // Convert root
155        let converter = WmoConverter::new();
156        converter.convert_root(&mut self.root, target_version)?;
157        self.root_modified = true;
158
159        // Convert all loaded groups
160        for (i, group) in self.groups.iter_mut().enumerate() {
161            converter.convert_group(group, target_version, self.original_version)?;
162            if i < self.group_modified.len() {
163                self.group_modified[i] = true;
164            }
165        }
166
167        Ok(())
168    }
169
170    /// Get the original version of the WMO
171    pub fn original_version(&self) -> WmoVersion {
172        self.original_version
173    }
174
175    /// Get the current version of the WMO
176    pub fn current_version(&self) -> WmoVersion {
177        self.root.version
178    }
179
180    /// Save the root WMO to a writer
181    pub fn save_root<W: std::io::Write + std::io::Seek>(&self, writer: &mut W) -> Result<()> {
182        // Write the root WMO
183        let writer_obj = WmoWriter::new();
184        writer_obj.write_root(writer, &self.root, self.root.version)?;
185
186        Ok(())
187    }
188
189    /// Save a specific group to a writer
190    pub fn save_group<W: std::io::Write + std::io::Seek>(
191        &self,
192        writer: &mut W,
193        index: usize,
194    ) -> Result<()> {
195        // Check if group exists
196        let group = self
197            .group(index)
198            .ok_or_else(|| WmoError::InvalidReference {
199                field: "group_index".to_string(),
200                value: index as u32,
201                max: self.groups.len() as u32 - 1,
202            })?;
203
204        // Write the group
205        let writer_obj = WmoWriter::new();
206        writer_obj.write_group(writer, group, self.root.version)?;
207
208        Ok(())
209    }
210
211    // Material editing methods
212
213    /// Add a new material
214    pub fn add_material(&mut self, material: WmoMaterial) -> usize {
215        self.root_modified = true;
216        self.root.materials.push(material);
217        self.root.header.n_materials += 1;
218        self.root.materials.len() - 1
219    }
220
221    /// Remove a material
222    pub fn remove_material(&mut self, index: usize) -> Result<WmoMaterial> {
223        if index >= self.root.materials.len() {
224            return Err(WmoError::InvalidReference {
225                field: "material_index".to_string(),
226                value: index as u32,
227                max: self.root.materials.len() as u32 - 1,
228            });
229        }
230
231        self.root_modified = true;
232        let material = self.root.materials.remove(index);
233        self.root.header.n_materials -= 1;
234
235        // Now we need to update all references to this material in groups
236        for (i, group) in self.groups.iter_mut().enumerate() {
237            let mut modified = false;
238
239            // Update materials list
240            for mat_idx in &mut group.materials {
241                match (*mat_idx as usize).cmp(&index) {
242                    std::cmp::Ordering::Equal => {
243                        // This material has been removed, use a default one instead
244                        *mat_idx = 0;
245                        modified = true;
246                    }
247                    std::cmp::Ordering::Greater => {
248                        // This material has been shifted down by one
249                        *mat_idx -= 1;
250                        modified = true;
251                    }
252                    std::cmp::Ordering::Less => {}
253                }
254            }
255
256            // Update batches
257            for batch in &mut group.batches {
258                match (batch.material_id as usize).cmp(&index) {
259                    std::cmp::Ordering::Equal => {
260                        // This material has been removed, use a default one instead
261                        batch.material_id = 0;
262                        modified = true;
263                    }
264                    std::cmp::Ordering::Greater => {
265                        // This material has been shifted down by one
266                        batch.material_id -= 1;
267                        modified = true;
268                    }
269                    std::cmp::Ordering::Less => {}
270                }
271            }
272
273            if modified && i < self.group_modified.len() {
274                self.group_modified[i] = true;
275            }
276        }
277
278        Ok(material)
279    }
280
281    /// Get a reference to a material
282    pub fn material(&self, index: usize) -> Option<&WmoMaterial> {
283        self.root.materials.get(index)
284    }
285
286    /// Get a mutable reference to a material
287    pub fn material_mut(&mut self, index: usize) -> Option<&mut WmoMaterial> {
288        self.root_modified = true;
289        self.root.materials.get_mut(index)
290    }
291
292    // Texture editing methods
293
294    /// Add a new texture
295    pub fn add_texture(&mut self, texture: String) -> usize {
296        self.root_modified = true;
297        self.root.textures.push(texture);
298        self.root.textures.len() - 1
299    }
300
301    /// Remove a texture
302    pub fn remove_texture(&mut self, index: usize) -> Result<String> {
303        if index >= self.root.textures.len() {
304            return Err(WmoError::InvalidReference {
305                field: "texture_index".to_string(),
306                value: index as u32,
307                max: self.root.textures.len() as u32 - 1,
308            });
309        }
310
311        self.root_modified = true;
312        let texture = self.root.textures.remove(index);
313
314        // Now we need to update all references to this texture in materials
315        for material in &mut self.root.materials {
316            match (material.texture1 as usize).cmp(&index) {
317                std::cmp::Ordering::Equal => {
318                    // This texture has been removed, use a default one instead
319                    material.texture1 = 0;
320                }
321                std::cmp::Ordering::Greater => {
322                    // This texture has been shifted down by one
323                    material.texture1 -= 1;
324                }
325                std::cmp::Ordering::Less => {}
326            }
327
328            match (material.texture2 as usize).cmp(&index) {
329                std::cmp::Ordering::Equal => {
330                    // This texture has been removed, use a default one instead
331                    material.texture2 = 0;
332                }
333                std::cmp::Ordering::Greater => {
334                    // This texture has been shifted down by one
335                    material.texture2 -= 1;
336                }
337                std::cmp::Ordering::Less => {}
338            }
339        }
340
341        Ok(texture)
342    }
343
344    /// Get a reference to a texture
345    pub fn texture(&self, index: usize) -> Option<&String> {
346        self.root.textures.get(index)
347    }
348
349    /// Get a mutable reference to a texture
350    pub fn texture_mut(&mut self, index: usize) -> Option<&mut String> {
351        self.root_modified = true;
352        self.root.textures.get_mut(index)
353    }
354
355    // Group editing methods
356
357    /// Create a new group
358    pub fn create_group(&mut self, name: String) -> usize {
359        self.root_modified = true;
360
361        // Create a new group info entry
362        let group_info = WmoGroupInfo {
363            flags: WmoGroupFlags::empty(),
364            bounding_box: BoundingBox {
365                min: Vec3 {
366                    x: 0.0,
367                    y: 0.0,
368                    z: 0.0,
369                },
370                max: Vec3 {
371                    x: 0.0,
372                    y: 0.0,
373                    z: 0.0,
374                },
375            },
376            name,
377        };
378
379        // Add to root
380        self.root.groups.push(group_info);
381        self.root.header.n_groups += 1;
382
383        // Create placeholder group data
384        let group_index = self.root.groups.len() - 1;
385        let header = WmoGroupHeader {
386            flags: WmoGroupFlags::empty(),
387            bounding_box: BoundingBox {
388                min: Vec3 {
389                    x: 0.0,
390                    y: 0.0,
391                    z: 0.0,
392                },
393                max: Vec3 {
394                    x: 0.0,
395                    y: 0.0,
396                    z: 0.0,
397                },
398            },
399            name_offset: 0, // Will be calculated when saving
400            group_index: group_index as u32,
401        };
402
403        let group = WmoGroup {
404            header,
405            materials: Vec::new(),
406            vertices: Vec::new(),
407            normals: Vec::new(),
408            tex_coords: Vec::new(),
409            batches: Vec::new(),
410            indices: Vec::new(),
411            vertex_colors: None,
412            bsp_nodes: None,
413            liquid: None,
414            doodad_refs: None,
415        };
416
417        // Add to groups
418        if self.groups.len() <= group_index {
419            self.groups.resize_with(group_index + 1, || WmoGroup {
420                header: WmoGroupHeader {
421                    flags: WmoGroupFlags::empty(),
422                    bounding_box: BoundingBox {
423                        min: Vec3 {
424                            x: 0.0,
425                            y: 0.0,
426                            z: 0.0,
427                        },
428                        max: Vec3 {
429                            x: 0.0,
430                            y: 0.0,
431                            z: 0.0,
432                        },
433                    },
434                    name_offset: 0,
435                    group_index: 0,
436                },
437                materials: Vec::new(),
438                vertices: Vec::new(),
439                normals: Vec::new(),
440                tex_coords: Vec::new(),
441                batches: Vec::new(),
442                indices: Vec::new(),
443                vertex_colors: None,
444                bsp_nodes: None,
445                liquid: None,
446                doodad_refs: None,
447            });
448        }
449
450        self.groups.push(group);
451        self.group_modified.push(true);
452
453        group_index
454    }
455
456    /// Remove a group
457    pub fn remove_group(&mut self, index: usize) -> Result<WmoGroupInfo> {
458        if index >= self.root.groups.len() {
459            return Err(WmoError::InvalidReference {
460                field: "group_index".to_string(),
461                value: index as u32,
462                max: self.root.groups.len() as u32 - 1,
463            });
464        }
465
466        self.root_modified = true;
467        let group_info = self.root.groups.remove(index);
468        self.root.header.n_groups -= 1;
469
470        // Update group indices
471        for (i, _group) in self.root.groups.iter_mut().enumerate() {
472            if i >= index {
473                // This group has been shifted down by one
474                let group_idx = i as u32;
475
476                if let Some(loaded_group) = self.groups.get_mut(i) {
477                    loaded_group.header.group_index = group_idx;
478                    if i < self.group_modified.len() {
479                        self.group_modified[i] = true;
480                    }
481                }
482            }
483        }
484
485        // Remove group data if loaded
486        if index < self.groups.len() {
487            self.groups.remove(index);
488        }
489
490        if index < self.group_modified.len() {
491            self.group_modified.remove(index);
492        }
493
494        // Update portal references
495        for portal_ref in &mut self.root.portal_references {
496            match (portal_ref.group_index as usize).cmp(&index) {
497                std::cmp::Ordering::Equal => {
498                    // This portal reference now points to a non-existent group
499                    // Setting to 0 is probably the safest option
500                    portal_ref.group_index = 0;
501                }
502                std::cmp::Ordering::Greater => {
503                    // This group has been shifted down by one
504                    portal_ref.group_index -= 1;
505                }
506                std::cmp::Ordering::Less => {}
507            }
508        }
509
510        Ok(group_info)
511    }
512
513    // Vertex manipulation methods
514
515    /// Add a vertex to a group
516    pub fn add_vertex(&mut self, group_index: usize, vertex: Vec3) -> Result<usize> {
517        // Validate group index
518        if group_index >= self.groups.len() {
519            return Err(WmoError::InvalidReference {
520                field: "group_index".to_string(),
521                value: group_index as u32,
522                max: self.groups.len() as u32 - 1,
523            });
524        }
525
526        // Work with the group
527        let vertex_index = {
528            let group = &mut self.groups[group_index];
529
530            // Add vertex
531            group.vertices.push(vertex);
532
533            // Update bounding box
534            let min = &mut group.header.bounding_box.min;
535            let max = &mut group.header.bounding_box.max;
536
537            min.x = min.x.min(vertex.x);
538            min.y = min.y.min(vertex.y);
539            min.z = min.z.min(vertex.z);
540
541            max.x = max.x.max(vertex.x);
542            max.y = max.y.max(vertex.y);
543            max.z = max.z.max(vertex.z);
544
545            group.vertices.len() - 1
546        };
547
548        // Also update root group info
549        if let Some(group_info) = self.root.groups.get_mut(group_index) {
550            let info_min = &mut group_info.bounding_box.min;
551            let info_max = &mut group_info.bounding_box.max;
552
553            info_min.x = info_min.x.min(vertex.x);
554            info_min.y = info_min.y.min(vertex.y);
555            info_min.z = info_min.z.min(vertex.z);
556
557            info_max.x = info_max.x.max(vertex.x);
558            info_max.y = info_max.y.max(vertex.y);
559            info_max.z = info_max.z.max(vertex.z);
560
561            self.root_modified = true;
562        }
563
564        Ok(vertex_index)
565    }
566
567    /// Remove a vertex from a group
568    pub fn remove_vertex(&mut self, group_index: usize, vertex_index: usize) -> Result<Vec3> {
569        // Validate group index
570        if group_index >= self.groups.len() {
571            return Err(WmoError::InvalidReference {
572                field: "group_index".to_string(),
573                value: group_index as u32,
574                max: self.groups.len() as u32 - 1,
575            });
576        }
577
578        let group = &mut self.groups[group_index];
579
580        if vertex_index >= group.vertices.len() {
581            return Err(WmoError::InvalidReference {
582                field: "vertex_index".to_string(),
583                value: vertex_index as u32,
584                max: group.vertices.len() as u32 - 1,
585            });
586        }
587
588        // Remove vertex
589        let vertex = group.vertices.remove(vertex_index);
590
591        // Remove corresponding normal if present
592        if vertex_index < group.normals.len() {
593            group.normals.remove(vertex_index);
594        }
595
596        // Remove corresponding texture coordinate if present
597        if vertex_index < group.tex_coords.len() {
598            group.tex_coords.remove(vertex_index);
599        }
600
601        // Remove corresponding vertex color if present
602        if let Some(colors) = &mut group.vertex_colors
603            && vertex_index < colors.len()
604        {
605            colors.remove(vertex_index);
606        }
607
608        // Update indices
609        for idx in &mut group.indices {
610            match (*idx as usize).cmp(&vertex_index) {
611                std::cmp::Ordering::Equal => {
612                    // This index now points to a non-existent vertex
613                    // Setting to 0 is probably the safest option
614                    *idx = 0;
615                }
616                std::cmp::Ordering::Greater => {
617                    // This index has been shifted down by one
618                    *idx -= 1;
619                }
620                std::cmp::Ordering::Less => {}
621            }
622        }
623
624        // Recalculate bounding box
625        self.recalculate_group_bounding_box(group_index)?;
626
627        Ok(vertex)
628    }
629
630    /// Recalculate the bounding box for a group
631    pub fn recalculate_group_bounding_box(&mut self, group_index: usize) -> Result<()> {
632        // Validate group index
633        if group_index >= self.groups.len() {
634            return Err(WmoError::InvalidReference {
635                field: "group_index".to_string(),
636                value: group_index as u32,
637                max: self.groups.len() as u32 - 1,
638            });
639        }
640
641        // Calculate the new bounding box
642        let new_bounding_box = {
643            let group = &mut self.groups[group_index];
644
645            if group.vertices.is_empty() {
646                // No vertices, use a default bounding box
647                BoundingBox {
648                    min: Vec3 {
649                        x: 0.0,
650                        y: 0.0,
651                        z: 0.0,
652                    },
653                    max: Vec3 {
654                        x: 0.0,
655                        y: 0.0,
656                        z: 0.0,
657                    },
658                }
659            } else {
660                // Calculate from vertices
661                let mut min_x = f32::MAX;
662                let mut min_y = f32::MAX;
663                let mut min_z = f32::MAX;
664                let mut max_x = f32::MIN;
665                let mut max_y = f32::MIN;
666                let mut max_z = f32::MIN;
667
668                for vertex in &group.vertices {
669                    min_x = min_x.min(vertex.x);
670                    min_y = min_y.min(vertex.y);
671                    min_z = min_z.min(vertex.z);
672
673                    max_x = max_x.max(vertex.x);
674                    max_y = max_y.max(vertex.y);
675                    max_z = max_z.max(vertex.z);
676                }
677
678                BoundingBox {
679                    min: Vec3 {
680                        x: min_x,
681                        y: min_y,
682                        z: min_z,
683                    },
684                    max: Vec3 {
685                        x: max_x,
686                        y: max_y,
687                        z: max_z,
688                    },
689                }
690            }
691        };
692
693        // Update the group's bounding box
694        self.groups[group_index].header.bounding_box = new_bounding_box;
695
696        // Also update root group info
697        if let Some(group_info) = self.root.groups.get_mut(group_index) {
698            group_info.bounding_box = new_bounding_box;
699            self.root_modified = true;
700        }
701
702        Ok(())
703    }
704
705    /// Recalculate the global bounding box
706    pub fn recalculate_global_bounding_box(&mut self) -> Result<()> {
707        if self.root.groups.is_empty() {
708            // No groups, use a default bounding box
709            self.root.bounding_box = BoundingBox {
710                min: Vec3 {
711                    x: 0.0,
712                    y: 0.0,
713                    z: 0.0,
714                },
715                max: Vec3 {
716                    x: 0.0,
717                    y: 0.0,
718                    z: 0.0,
719                },
720            };
721        } else {
722            // Calculate from group bounding boxes
723            let mut min_x = f32::MAX;
724            let mut min_y = f32::MAX;
725            let mut min_z = f32::MAX;
726            let mut max_x = f32::MIN;
727            let mut max_y = f32::MIN;
728            let mut max_z = f32::MIN;
729
730            for group_info in &self.root.groups {
731                min_x = min_x.min(group_info.bounding_box.min.x);
732                min_y = min_y.min(group_info.bounding_box.min.y);
733                min_z = min_z.min(group_info.bounding_box.min.z);
734
735                max_x = max_x.max(group_info.bounding_box.max.x);
736                max_y = max_y.max(group_info.bounding_box.max.y);
737                max_z = max_z.max(group_info.bounding_box.max.z);
738            }
739
740            self.root.bounding_box = BoundingBox {
741                min: Vec3 {
742                    x: min_x,
743                    y: min_y,
744                    z: min_z,
745                },
746                max: Vec3 {
747                    x: max_x,
748                    y: max_y,
749                    z: max_z,
750                },
751            };
752        }
753
754        self.root_modified = true;
755        Ok(())
756    }
757
758    // Doodad manipulation methods
759
760    /// Add a doodad definition
761    pub fn add_doodad(&mut self, doodad: WmoDoodadDef) -> usize {
762        self.root_modified = true;
763        self.root.doodad_defs.push(doodad);
764        self.root.header.n_doodad_defs += 1;
765        self.root.header.n_doodad_names += 1; // Assuming name is also added
766
767        self.root.doodad_defs.len() - 1
768    }
769
770    /// Remove a doodad definition
771    pub fn remove_doodad(&mut self, index: usize) -> Result<WmoDoodadDef> {
772        if index >= self.root.doodad_defs.len() {
773            return Err(WmoError::InvalidReference {
774                field: "doodad_index".to_string(),
775                value: index as u32,
776                max: self.root.doodad_defs.len() as u32 - 1,
777            });
778        }
779
780        self.root_modified = true;
781        let doodad = self.root.doodad_defs.remove(index);
782        self.root.header.n_doodad_defs -= 1;
783
784        // Update doodad references in sets
785        for set in &mut self.root.doodad_sets {
786            if set.start_doodad as usize <= index
787                && index < (set.start_doodad + set.n_doodads) as usize
788            {
789                // This doodad was part of the set
790                set.n_doodads -= 1;
791            }
792
793            if set.start_doodad as usize > index {
794                // Doodads after the removed one have shifted down
795                set.start_doodad -= 1;
796            }
797        }
798
799        // Update doodad references in groups
800        for (i, group) in self.groups.iter_mut().enumerate() {
801            if let Some(refs) = &mut group.doodad_refs {
802                let mut modified = false;
803
804                for doodad_ref in refs.iter_mut() {
805                    match (*doodad_ref as usize).cmp(&index) {
806                        std::cmp::Ordering::Equal => {
807                            // This reference now points to a non-existent doodad
808                            // Remove this reference
809                            *doodad_ref = 0;
810                            modified = true;
811                        }
812                        std::cmp::Ordering::Greater => {
813                            // This reference has been shifted down by one
814                            *doodad_ref -= 1;
815                            modified = true;
816                        }
817                        std::cmp::Ordering::Less => {
818                            // No change needed
819                        }
820                    }
821                }
822
823                if modified && i < self.group_modified.len() {
824                    self.group_modified[i] = true;
825                }
826            }
827        }
828
829        Ok(doodad)
830    }
831
832    /// Doodad set manipulation methods
833    /// Add a doodad set
834    pub fn add_doodad_set(&mut self, set: WmoDoodadSet) -> usize {
835        self.root_modified = true;
836        self.root.doodad_sets.push(set);
837        self.root.header.n_doodad_sets += 1;
838
839        self.root.doodad_sets.len() - 1
840    }
841
842    /// Remove a doodad set
843    pub fn remove_doodad_set(&mut self, index: usize) -> Result<WmoDoodadSet> {
844        if index >= self.root.doodad_sets.len() {
845            return Err(WmoError::InvalidReference {
846                field: "doodad_set_index".to_string(),
847                value: index as u32,
848                max: self.root.doodad_sets.len() as u32 - 1,
849            });
850        }
851
852        self.root_modified = true;
853        let set = self.root.doodad_sets.remove(index);
854        self.root.header.n_doodad_sets -= 1;
855
856        Ok(set)
857    }
858}