Skip to main content

eldiron_shared/
project.rs

1use crate::prelude::*;
2use buildergraph::BuilderGraph;
3use indexmap::IndexMap;
4pub use rusterix::map::*;
5use theframework::prelude::*;
6use tilegraph::TileGraphPaletteSource;
7
8/// The default target fps for the game.
9fn default_target_fps() -> u32 {
10    30
11}
12
13/// The default ms per tick for the game.
14fn default_tick_ms() -> u32 {
15    250
16}
17
18fn default_rules() -> String {
19    String::new()
20}
21
22fn default_locales() -> String {
23    String::new()
24}
25
26fn default_audio_fx() -> String {
27    String::new()
28}
29
30fn default_authoring() -> String {
31    String::new()
32}
33
34fn default_tile_board_cols() -> i32 {
35    13
36}
37
38fn default_tile_board_rows() -> i32 {
39    9
40}
41
42fn default_tile_collection_name() -> String {
43    "New Collection".to_string()
44}
45
46fn default_tile_collection_version() -> String {
47    "0.1".to_string()
48}
49
50fn default_palette_material_slots() -> Vec<PaletteMaterial> {
51    vec![PaletteMaterial::default(); 256]
52}
53
54#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
55pub struct PaletteMaterial {
56    #[serde(default = "default_palette_roughness")]
57    pub roughness: f32,
58    #[serde(default = "default_palette_metallic")]
59    pub metallic: f32,
60    #[serde(default = "default_palette_opacity")]
61    pub opacity: f32,
62    #[serde(default = "default_palette_emissive")]
63    pub emissive: f32,
64}
65
66fn default_palette_roughness() -> f32 {
67    0.5
68}
69
70fn default_palette_metallic() -> f32 {
71    0.0
72}
73
74fn default_palette_opacity() -> f32 {
75    1.0
76}
77
78fn default_palette_emissive() -> f32 {
79    0.0
80}
81
82impl Default for PaletteMaterial {
83    fn default() -> Self {
84        Self {
85            roughness: default_palette_roughness(),
86            metallic: default_palette_metallic(),
87            opacity: default_palette_opacity(),
88            emissive: default_palette_emissive(),
89        }
90    }
91}
92
93#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
94pub struct BuilderGraphAsset {
95    pub id: Uuid,
96    pub graph_id: Uuid,
97    #[serde(default)]
98    pub graph_name: String,
99    #[serde(default)]
100    pub graph_data: String,
101}
102
103impl BuilderGraphAsset {
104    pub fn new_table(name: String) -> Self {
105        let graph_data = BuilderGraph::preset_table_script_named(name.clone());
106        let graph_name = if let Ok(document) = buildergraph::BuilderDocument::from_text(&graph_data)
107        {
108            document.name().to_string()
109        } else if name.is_empty() {
110            "Table".to_string()
111        } else {
112            name.clone()
113        };
114        Self {
115            id: Uuid::new_v4(),
116            graph_id: Uuid::new_v4(),
117            graph_name,
118            graph_data,
119        }
120    }
121
122    pub fn new_empty(name: String) -> Self {
123        let graph_data = BuilderGraph::empty_script_named(name.clone());
124        let graph_name = if let Ok(document) = buildergraph::BuilderDocument::from_text(&graph_data)
125        {
126            document.name().to_string()
127        } else if name.is_empty() {
128            "Empty".to_string()
129        } else {
130            name.clone()
131        };
132        Self {
133            id: Uuid::new_v4(),
134            graph_id: Uuid::new_v4(),
135            graph_name,
136            graph_data,
137        }
138    }
139
140    pub fn new_wall_torch(name: String) -> Self {
141        let graph_data = BuilderGraph::preset_wall_torch_script_named(name.clone());
142        let graph_name = if let Ok(document) = buildergraph::BuilderDocument::from_text(&graph_data)
143        {
144            document.name().to_string()
145        } else if name.is_empty() {
146            "Wall Torch".to_string()
147        } else {
148            name.clone()
149        };
150        Self {
151            id: Uuid::new_v4(),
152            graph_id: Uuid::new_v4(),
153            graph_name,
154            graph_data,
155        }
156    }
157
158    pub fn new_wall_lantern(name: String) -> Self {
159        let graph_data = BuilderGraph::preset_wall_lantern_script_named(name.clone());
160        let graph_name = if let Ok(document) = buildergraph::BuilderDocument::from_text(&graph_data)
161        {
162            document.name().to_string()
163        } else if name.is_empty() {
164            "Wall Lantern".to_string()
165        } else {
166            name.clone()
167        };
168        Self {
169            id: Uuid::new_v4(),
170            graph_id: Uuid::new_v4(),
171            graph_name,
172            graph_data,
173        }
174    }
175
176    pub fn new_campfire(name: String) -> Self {
177        let graph_data = BuilderGraph::preset_campfire_script_named(name.clone());
178        let graph_name = if let Ok(document) = buildergraph::BuilderDocument::from_text(&graph_data)
179        {
180            document.name().to_string()
181        } else if name.is_empty() {
182            "Campfire".to_string()
183        } else {
184            name.clone()
185        };
186        Self {
187            id: Uuid::new_v4(),
188            graph_id: Uuid::new_v4(),
189            graph_name,
190            graph_data,
191        }
192    }
193}
194
195#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
196pub struct NodeGroupAsset {
197    pub group_id: Uuid,
198    pub graph_id: Uuid,
199    #[serde(default)]
200    pub graph_name: String,
201    pub output_grid_width: u16,
202    pub output_grid_height: u16,
203    pub tile_pixel_width: u16,
204    pub tile_pixel_height: u16,
205    #[serde(default)]
206    pub palette_source: TileGraphPaletteSource,
207    #[serde(default)]
208    pub palette_colors: Vec<TheColor>,
209    #[serde(default)]
210    pub graph_data: String,
211}
212
213impl NodeGroupAsset {
214    pub fn new(
215        group_id: Uuid,
216        output_grid_width: u16,
217        output_grid_height: u16,
218        palette_colors: Vec<TheColor>,
219    ) -> Self {
220        Self {
221            group_id,
222            graph_id: Uuid::new_v4(),
223            graph_name: "New Node Graph".to_string(),
224            output_grid_width,
225            output_grid_height,
226            tile_pixel_width: 32,
227            tile_pixel_height: 32,
228            palette_source: TileGraphPaletteSource::Local,
229            palette_colors,
230            graph_data: String::new(),
231        }
232    }
233}
234
235#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
236pub enum TileCollectionEntry {
237    SingleTile(Uuid),
238    TileGroup(Uuid),
239}
240
241impl TileCollectionEntry {
242    pub fn matches_source(&self, source: rusterix::TileSource) -> bool {
243        match (self, source) {
244            (Self::SingleTile(a), rusterix::TileSource::SingleTile(b)) => *a == b,
245            (Self::TileGroup(a), rusterix::TileSource::TileGroup(b)) => *a == b,
246            _ => false,
247        }
248    }
249}
250
251#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
252pub struct TileCollectionAsset {
253    pub id: Uuid,
254    #[serde(default = "default_tile_collection_name")]
255    pub name: String,
256    #[serde(default)]
257    pub author: String,
258    #[serde(default = "default_tile_collection_version")]
259    pub version: String,
260    #[serde(default)]
261    pub description: String,
262    #[serde(default)]
263    pub entries: Vec<TileCollectionEntry>,
264    #[serde(default)]
265    pub tile_board_tiles: IndexMap<Uuid, Vec2<i32>>,
266    #[serde(default)]
267    pub tile_board_groups: IndexMap<Uuid, Vec2<i32>>,
268    #[serde(default)]
269    pub tile_board_empty_slots: Vec<Vec2<i32>>,
270}
271
272impl TileCollectionAsset {
273    pub fn new(name: String) -> Self {
274        Self {
275            id: Uuid::new_v4(),
276            name,
277            author: String::new(),
278            version: default_tile_collection_version(),
279            description: String::new(),
280            entries: Vec::new(),
281            tile_board_tiles: IndexMap::default(),
282            tile_board_groups: IndexMap::default(),
283            tile_board_empty_slots: Vec::new(),
284        }
285    }
286}
287
288#[derive(Serialize, Deserialize, Clone, Debug)]
289pub struct Project {
290    pub name: String,
291    pub regions: Vec<Region>,
292    pub tilemaps: Vec<Tilemap>,
293
294    /// Tiles in the project
295    #[serde(default)]
296    pub tiles: IndexMap<Uuid, rusterix::Tile>,
297
298    /// Spatial tile groups in the project.
299    #[serde(default)]
300    pub tile_groups: IndexMap<Uuid, rusterix::TileGroup>,
301
302    /// Node-backed tile groups keyed by tile-group id.
303    #[serde(default)]
304    pub tile_node_groups: IndexMap<Uuid, NodeGroupAsset>,
305
306    /// Standalone builder graphs for props and assemblies.
307    #[serde(default)]
308    pub builder_graphs: IndexMap<Uuid, BuilderGraphAsset>,
309
310    /// Custom top-level tile collections shown as tabs in the tile picker.
311    #[serde(default)]
312    pub tile_collections: IndexMap<Uuid, TileCollectionAsset>,
313
314    /// Persisted board positions for top-level single tiles in the tile picker.
315    #[serde(default)]
316    pub tile_board_tiles: IndexMap<Uuid, Vec2<i32>>,
317
318    /// Persisted board positions for top-level tile groups in the tile picker.
319    #[serde(default)]
320    pub tile_board_groups: IndexMap<Uuid, Vec2<i32>>,
321
322    /// Persisted empty board cells left behind by deletions in the tile picker.
323    #[serde(default)]
324    pub tile_board_empty_slots: Vec<Vec2<i32>>,
325
326    /// Total board width in cells, including the trailing empty strip.
327    #[serde(default = "default_tile_board_cols")]
328    pub tile_board_cols: i32,
329
330    /// Total board height in cells, including the trailing empty strip.
331    #[serde(default = "default_tile_board_rows")]
332    pub tile_board_rows: i32,
333
334    #[serde(default)]
335    pub time: TheTime,
336
337    #[serde(default)]
338    pub characters: IndexMap<Uuid, Character>,
339    #[serde(default)]
340    pub items: IndexMap<Uuid, Item>,
341
342    #[serde(default)]
343    pub screens: IndexMap<Uuid, Screen>,
344
345    #[serde(default)]
346    pub assets: IndexMap<Uuid, Asset>,
347
348    #[serde(default)]
349    pub palette: ThePalette,
350
351    #[serde(default = "default_target_fps")]
352    pub target_fps: u32,
353
354    #[serde(default = "default_tick_ms")]
355    pub tick_ms: u32,
356
357    #[serde(default)]
358    pub config: String,
359
360    #[serde(default = "default_rules")]
361    pub rules: String,
362
363    #[serde(default = "default_locales")]
364    pub locales: String,
365
366    #[serde(default = "default_audio_fx")]
367    pub audio_fx: String,
368
369    #[serde(default = "default_authoring")]
370    pub authoring: String,
371
372    #[serde(default)]
373    pub avatars: IndexMap<Uuid, Avatar>,
374
375    #[serde(default = "default_palette_material_slots")]
376    pub palette_materials: Vec<PaletteMaterial>,
377}
378
379impl Default for Project {
380    fn default() -> Self {
381        Self::new()
382    }
383}
384
385impl Project {
386    pub fn new() -> Self {
387        let region = Region::default();
388
389        Self {
390            name: String::new(),
391
392            regions: vec![region],
393            tilemaps: vec![],
394
395            tiles: IndexMap::default(),
396            tile_groups: IndexMap::default(),
397            tile_node_groups: IndexMap::default(),
398            builder_graphs: IndexMap::default(),
399            tile_collections: IndexMap::default(),
400            tile_board_tiles: IndexMap::default(),
401            tile_board_groups: IndexMap::default(),
402            tile_board_empty_slots: Vec::new(),
403            tile_board_cols: default_tile_board_cols(),
404            tile_board_rows: default_tile_board_rows(),
405
406            time: TheTime::default(),
407
408            characters: IndexMap::default(),
409            items: IndexMap::default(),
410
411            screens: IndexMap::default(),
412            assets: IndexMap::default(),
413
414            palette: ThePalette::default(),
415
416            target_fps: default_target_fps(),
417            tick_ms: default_tick_ms(),
418
419            avatars: IndexMap::default(),
420            palette_materials: default_palette_material_slots(),
421
422            config: String::new(),
423            rules: default_rules(),
424            locales: default_locales(),
425            audio_fx: default_audio_fx(),
426            authoring: default_authoring(),
427        }
428    }
429
430    /// Add Character
431    pub fn add_character(&mut self, character: Character) {
432        self.characters.insert(character.id, character);
433    }
434
435    pub fn ensure_palette_materials_len(&mut self) {
436        if self.palette_materials.len() < self.palette.colors.len() {
437            self.palette_materials
438                .resize(self.palette.colors.len(), PaletteMaterial::default());
439        } else if self.palette_materials.len() > self.palette.colors.len() {
440            self.palette_materials.truncate(self.palette.colors.len());
441        }
442    }
443
444    pub fn reset_palette_material(&mut self, index: usize) {
445        self.ensure_palette_materials_len();
446        if let Some(material) = self.palette_materials.get_mut(index) {
447            *material = PaletteMaterial::default();
448        }
449    }
450
451    pub fn reset_all_palette_materials(&mut self) {
452        self.palette_materials = default_palette_material_slots();
453        self.ensure_palette_materials_len();
454    }
455
456    /// Removes the given character from the project.
457    pub fn remove_character(&mut self, id: &Uuid) {
458        self.characters.shift_remove(id);
459    }
460
461    /// Returns a list of all characters sorted by name.
462    pub fn sorted_character_list(&self) -> Vec<(Uuid, String)> {
463        let mut entries: Vec<(Uuid, String)> = self
464            .characters
465            .iter()
466            .map(|(uuid, data)| (*uuid, data.name.clone()))
467            .collect();
468
469        entries.sort_by(|a, b| a.1.cmp(&b.1));
470        entries
471    }
472
473    /// Returns a list of all items sorted by name.
474    pub fn sorted_item_list(&self) -> Vec<(Uuid, String)> {
475        let mut entries: Vec<(Uuid, String)> = self
476            .items
477            .iter()
478            .map(|(uuid, data)| (*uuid, data.name.clone()))
479            .collect();
480
481        entries.sort_by(|a, b| a.1.cmp(&b.1));
482        entries
483    }
484
485    /// Add Avatar
486    pub fn add_avatar(&mut self, avatar: Avatar) {
487        self.avatars.insert(avatar.id, avatar);
488    }
489
490    pub fn add_tile_group(&mut self, tile_group: rusterix::TileGroup) {
491        self.tile_groups.insert(tile_group.id, tile_group);
492    }
493
494    pub fn add_tile_node_group(&mut self, node_group: NodeGroupAsset) {
495        self.tile_node_groups
496            .insert(node_group.group_id, node_group);
497    }
498
499    pub fn add_builder_graph(&mut self, builder_graph: BuilderGraphAsset) {
500        self.builder_graphs.insert(builder_graph.id, builder_graph);
501    }
502
503    pub fn add_tile_collection(&mut self, collection: TileCollectionAsset) {
504        self.tile_collections.insert(collection.id, collection);
505    }
506
507    pub fn is_tile_node_group(&self, id: &Uuid) -> bool {
508        self.tile_node_groups.contains_key(id)
509    }
510
511    pub fn collection_contains_source(
512        &self,
513        collection_id: &Uuid,
514        source: rusterix::TileSource,
515    ) -> bool {
516        self.tile_collections
517            .get(collection_id)
518            .map(|collection| {
519                collection
520                    .entries
521                    .iter()
522                    .any(|entry| entry.matches_source(source))
523            })
524            .unwrap_or(false)
525    }
526
527    pub fn add_source_to_collection(&mut self, collection_id: &Uuid, source: rusterix::TileSource) {
528        let Some(collection) = self.tile_collections.get_mut(collection_id) else {
529            return;
530        };
531        let entry = match source {
532            rusterix::TileSource::SingleTile(id) => TileCollectionEntry::SingleTile(id),
533            rusterix::TileSource::TileGroup(id) => TileCollectionEntry::TileGroup(id),
534            _ => return,
535        };
536        if !collection.entries.contains(&entry) {
537            collection.entries.push(entry);
538        }
539    }
540
541    pub fn remove_source_from_collections(&mut self, source: rusterix::TileSource) {
542        for collection in self.tile_collections.values_mut() {
543            collection
544                .entries
545                .retain(|entry| !entry.matches_source(source));
546            match source {
547                rusterix::TileSource::SingleTile(id) => {
548                    collection.tile_board_tiles.shift_remove(&id);
549                }
550                rusterix::TileSource::TileGroup(id) => {
551                    collection.tile_board_groups.shift_remove(&id);
552                }
553                _ => {}
554            }
555        }
556    }
557
558    pub fn remove_tile_group(&mut self, id: &Uuid) {
559        self.tile_groups.shift_remove(id);
560        self.tile_node_groups.shift_remove(id);
561        self.tile_board_groups.shift_remove(id);
562        self.remove_source_from_collections(rusterix::TileSource::TileGroup(*id));
563    }
564
565    pub fn tile_board_position(&self, source: rusterix::TileSource) -> Option<Vec2<i32>> {
566        match source {
567            rusterix::TileSource::SingleTile(id) => self.tile_board_tiles.get(&id).copied(),
568            rusterix::TileSource::TileGroup(id) => self.tile_board_groups.get(&id).copied(),
569            _ => None,
570        }
571    }
572
573    pub fn collection_tile_board_position(
574        &self,
575        collection_id: &Uuid,
576        source: rusterix::TileSource,
577    ) -> Option<Vec2<i32>> {
578        let collection = self.tile_collections.get(collection_id)?;
579        match source {
580            rusterix::TileSource::SingleTile(id) => collection.tile_board_tiles.get(&id).copied(),
581            rusterix::TileSource::TileGroup(id) => collection.tile_board_groups.get(&id).copied(),
582            _ => None,
583        }
584    }
585
586    pub fn tile_board_empty_slots(&self) -> &[Vec2<i32>] {
587        &self.tile_board_empty_slots
588    }
589
590    pub fn collection_tile_board_empty_slots(&self, collection_id: &Uuid) -> Option<&[Vec2<i32>]> {
591        self.tile_collections
592            .get(collection_id)
593            .map(|collection| collection.tile_board_empty_slots.as_slice())
594    }
595
596    pub fn set_tile_board_position(&mut self, source: rusterix::TileSource, pos: Vec2<i32>) {
597        self.clear_tile_board_empty_slot(pos);
598        match source {
599            rusterix::TileSource::SingleTile(id) => {
600                self.tile_board_tiles.insert(id, pos);
601            }
602            rusterix::TileSource::TileGroup(id) => {
603                self.tile_board_groups.insert(id, pos);
604            }
605            _ => {}
606        }
607    }
608
609    pub fn set_collection_tile_board_position(
610        &mut self,
611        collection_id: &Uuid,
612        source: rusterix::TileSource,
613        pos: Vec2<i32>,
614    ) {
615        let Some(collection) = self.tile_collections.get_mut(collection_id) else {
616            return;
617        };
618        if let Some(index) = collection
619            .tile_board_empty_slots
620            .iter()
621            .position(|p| *p == pos)
622        {
623            collection.tile_board_empty_slots.swap_remove(index);
624        }
625        match source {
626            rusterix::TileSource::SingleTile(id) => {
627                collection.tile_board_tiles.insert(id, pos);
628            }
629            rusterix::TileSource::TileGroup(id) => {
630                collection.tile_board_groups.insert(id, pos);
631            }
632            _ => {}
633        }
634    }
635
636    pub fn reserve_tile_board_empty_slot(&mut self, pos: Vec2<i32>) {
637        if !self.tile_board_empty_slots.contains(&pos) {
638            self.tile_board_empty_slots.push(pos);
639        }
640    }
641
642    pub fn reserve_collection_tile_board_empty_slot(
643        &mut self,
644        collection_id: &Uuid,
645        pos: Vec2<i32>,
646    ) {
647        let Some(collection) = self.tile_collections.get_mut(collection_id) else {
648            return;
649        };
650        if !collection.tile_board_empty_slots.contains(&pos) {
651            collection.tile_board_empty_slots.push(pos);
652        }
653    }
654
655    pub fn clear_tile_board_empty_slot(&mut self, pos: Vec2<i32>) {
656        if let Some(index) = self.tile_board_empty_slots.iter().position(|p| *p == pos) {
657            self.tile_board_empty_slots.swap_remove(index);
658        }
659    }
660
661    pub fn ensure_tile_board_space(&mut self, pos: Vec2<i32>) {
662        if pos.x >= self.tile_board_cols - 1 {
663            self.tile_board_cols = pos.x + 2;
664        }
665        if pos.y >= self.tile_board_rows - 1 {
666            self.tile_board_rows = pos.y + 2;
667        }
668    }
669
670    /// Removes the given avatar from the project.
671    pub fn remove_avatar(&mut self, id: &Uuid) {
672        self.avatars.shift_remove(id);
673    }
674
675    /// Finds the avatar that contains the given animation id.
676    pub fn find_avatar_for_animation(&self, animation_id: &Uuid) -> Option<&Avatar> {
677        self.avatars
678            .values()
679            .find(|a| a.animations.iter().any(|anim| anim.id == *animation_id))
680    }
681
682    /// Returns an immutable reference to the texture identified by the editing context.
683    pub fn get_editing_texture(
684        &self,
685        editing_ctx: &PixelEditingContext,
686    ) -> Option<&rusterix::Texture> {
687        match editing_ctx {
688            PixelEditingContext::None => None,
689            PixelEditingContext::Tile(tile_id, frame_index) => {
690                let tile = self.tiles.get(tile_id)?;
691                tile.textures.get(*frame_index)
692            }
693            PixelEditingContext::AvatarFrame(
694                avatar_id,
695                anim_id,
696                perspective_index,
697                frame_index,
698            ) => {
699                let avatar = self.avatars.get(avatar_id)?;
700                let anim = avatar.animations.iter().find(|a| a.id == *anim_id)?;
701                let perspective = anim.perspectives.get(*perspective_index)?;
702                perspective.frames.get(*frame_index).map(|f| &f.texture)
703            }
704        }
705    }
706
707    /// Returns a mutable reference to the texture identified by the editing context.
708    pub fn get_editing_texture_mut(
709        &mut self,
710        editing_ctx: &PixelEditingContext,
711    ) -> Option<&mut rusterix::Texture> {
712        match editing_ctx {
713            PixelEditingContext::None => None,
714            PixelEditingContext::Tile(tile_id, frame_index) => {
715                let tile = self.tiles.get_mut(tile_id)?;
716                tile.textures.get_mut(*frame_index)
717            }
718            PixelEditingContext::AvatarFrame(
719                avatar_id,
720                anim_id,
721                perspective_index,
722                frame_index,
723            ) => {
724                let avatar = self.avatars.get_mut(avatar_id)?;
725                let anim = avatar.animations.iter_mut().find(|a| a.id == *anim_id)?;
726                let perspective = anim.perspectives.get_mut(*perspective_index)?;
727                perspective
728                    .frames
729                    .get_mut(*frame_index)
730                    .map(|f| &mut f.texture)
731            }
732        }
733    }
734
735    /// Returns an immutable avatar frame for avatar frame editing contexts.
736    pub fn get_editing_avatar_frame(
737        &self,
738        editing_ctx: &PixelEditingContext,
739    ) -> Option<&rusterix::AvatarAnimationFrame> {
740        match editing_ctx {
741            PixelEditingContext::AvatarFrame(
742                avatar_id,
743                anim_id,
744                perspective_index,
745                frame_index,
746            ) => {
747                let avatar = self.avatars.get(avatar_id)?;
748                let anim = avatar.animations.iter().find(|a| a.id == *anim_id)?;
749                let perspective = anim.perspectives.get(*perspective_index)?;
750                perspective.frames.get(*frame_index)
751            }
752            _ => None,
753        }
754    }
755
756    /// Returns a mutable avatar frame for avatar frame editing contexts.
757    pub fn get_editing_avatar_frame_mut(
758        &mut self,
759        editing_ctx: &PixelEditingContext,
760    ) -> Option<&mut rusterix::AvatarAnimationFrame> {
761        match editing_ctx {
762            PixelEditingContext::AvatarFrame(
763                avatar_id,
764                anim_id,
765                perspective_index,
766                frame_index,
767            ) => {
768                let avatar = self.avatars.get_mut(avatar_id)?;
769                let anim = avatar.animations.iter_mut().find(|a| a.id == *anim_id)?;
770                let perspective = anim.perspectives.get_mut(*perspective_index)?;
771                perspective.frames.get_mut(*frame_index)
772            }
773            _ => None,
774        }
775    }
776
777    /// Returns an immutable avatar perspective for avatar frame editing contexts.
778    pub fn get_editing_avatar_perspective(
779        &self,
780        editing_ctx: &PixelEditingContext,
781    ) -> Option<&rusterix::AvatarPerspective> {
782        match editing_ctx {
783            PixelEditingContext::AvatarFrame(avatar_id, anim_id, perspective_index, _) => {
784                let avatar = self.avatars.get(avatar_id)?;
785                let anim = avatar.animations.iter().find(|a| a.id == *anim_id)?;
786                anim.perspectives.get(*perspective_index)
787            }
788            _ => None,
789        }
790    }
791
792    /// Returns a mutable avatar perspective for avatar frame editing contexts.
793    pub fn get_editing_avatar_perspective_mut(
794        &mut self,
795        editing_ctx: &PixelEditingContext,
796    ) -> Option<&mut rusterix::AvatarPerspective> {
797        match editing_ctx {
798            PixelEditingContext::AvatarFrame(avatar_id, anim_id, perspective_index, _) => {
799                let avatar = self.avatars.get_mut(avatar_id)?;
800                let anim = avatar.animations.iter_mut().find(|a| a.id == *anim_id)?;
801                anim.perspectives.get_mut(*perspective_index)
802            }
803            _ => None,
804        }
805    }
806
807    /// Add Item
808    pub fn add_item(&mut self, item: Item) {
809        self.items.insert(item.id, item);
810    }
811
812    /// Removes the given item from the project.
813    pub fn remove_item(&mut self, id: &Uuid) {
814        self.items.shift_remove(id);
815    }
816
817    /// Add a tilemap
818    pub fn add_tilemap(&mut self, tilemap: Tilemap) {
819        self.tilemaps.push(tilemap)
820    }
821
822    /// Get the tilemap of the given uuid.
823    pub fn get_tilemap(&self, uuid: Uuid) -> Option<&Tilemap> {
824        self.tilemaps.iter().find(|t| t.id == uuid)
825    }
826
827    /// Get the tilemap of the given uuid.
828    pub fn get_tilemap_mut(&mut self, uuid: Uuid) -> Option<&mut Tilemap> {
829        self.tilemaps.iter_mut().find(|t| t.id == uuid)
830    }
831
832    /// Removes the given tilemap from the project.
833    pub fn remove_tilemap(&mut self, id: TheId) {
834        self.tilemaps.retain(|item| item.id != id.uuid);
835    }
836
837    /// Contains the region of the given uuid.
838    pub fn contains_region(&self, uuid: &Uuid) -> bool {
839        self.regions.iter().find(|t| t.id == *uuid).is_some()
840    }
841
842    /// Get the region of the given uuid.
843    pub fn get_region(&self, uuid: &Uuid) -> Option<&Region> {
844        self.regions.iter().find(|t| t.id == *uuid)
845    }
846
847    /// Get the region of the given uuid as mutable.
848    pub fn get_region_mut(&mut self, uuid: &Uuid) -> Option<&mut Region> {
849        self.regions.iter_mut().find(|t| t.id == *uuid)
850    }
851
852    /// Get the region of the given uuid.
853    pub fn get_region_ctx(&self, ctx: &ServerContext) -> Option<&Region> {
854        self.regions.iter().find(|t| t.id == ctx.curr_region)
855    }
856
857    /// Get the region of the given uuid as mutable.
858    pub fn get_region_ctx_mut(&mut self, ctx: &ServerContext) -> Option<&mut Region> {
859        self.regions.iter_mut().find(|t| t.id == ctx.curr_region)
860    }
861
862    /// Get the screen of the given uuid.
863    pub fn get_screen_ctx(&self, ctx: &ServerContext) -> Option<&Screen> {
864        self.screens.get(&ctx.curr_screen)
865    }
866
867    /// Get the mut screen of the given uuid.
868    pub fn get_screen_ctx_mut(&mut self, ctx: &ServerContext) -> Option<&mut Screen> {
869        self.screens.get_mut(&ctx.curr_screen)
870    }
871
872    /// Remove a region
873    pub fn remove_region(&mut self, id: &Uuid) {
874        self.regions.retain(|item| item.id != *id);
875    }
876
877    /// Get the map of the current context.
878    pub fn get_map(&self, ctx: &ServerContext) -> Option<&Map> {
879        if ctx.editor_view_mode != EditorViewMode::D2 {
880            if let Some(region) = self.get_region(&ctx.curr_region) {
881                if ctx.geometry_edit_mode == GeometryEditMode::Detail {
882                    if let Some(surface) = ctx.active_detail_surface.as_ref() {
883                        if let Some(surface) = region.map.surfaces.get(&surface.id) {
884                            if let Some(profile_id) = surface.profile {
885                                return region.map.profiles.get(&profile_id);
886                            }
887                        }
888                    }
889                }
890                return Some(&region.map);
891            }
892        } else if ctx.get_map_context() == MapContext::Region {
893            let id = ctx.curr_region;
894            // if let Some(id) = ctx.pc.id() {
895            if let Some(surface) = &ctx.editing_surface {
896                if let Some(region) = self.regions.iter().find(|t| t.id == id) {
897                    if let Some(surface) = region.map.surfaces.get(&surface.id) {
898                        if let Some(profile_id) = surface.profile {
899                            return region.map.profiles.get(&profile_id);
900                        }
901                    }
902                }
903                return None;
904            } else if let Some(region) = self.regions.iter().find(|t| t.id == id) {
905                return Some(&region.map);
906            }
907            // }
908        } else if ctx.get_map_context() == MapContext::Screen {
909            if let Some(id) = ctx.pc.id() {
910                if let Some(screen) = self.screens.get(&id) {
911                    return Some(&screen.map);
912                }
913            }
914        } else if ctx.get_map_context() == MapContext::Character {
915            if let ContentContext::CharacterTemplate(id) = ctx.curr_character {
916                if let Some(character) = self.characters.get(&id) {
917                    return Some(&character.map);
918                }
919            }
920        } else if ctx.get_map_context() == MapContext::Item {
921            if let ContentContext::ItemTemplate(id) = ctx.curr_item {
922                if let Some(item) = self.items.get(&id) {
923                    return Some(&item.map);
924                }
925            }
926        }
927        None
928    }
929
930    /// Get the mutable map of the current context.
931    pub fn get_map_mut(&mut self, ctx: &ServerContext) -> Option<&mut Map> {
932        if ctx.get_map_context() == MapContext::Region {
933            let id = ctx.curr_region;
934            // if let Some(id) = ctx.pc.id() {
935            if ctx.editor_view_mode != EditorViewMode::D2 {
936                if let Some(region) = self.get_region_mut(&ctx.curr_region) {
937                    if ctx.geometry_edit_mode == GeometryEditMode::Detail {
938                        if let Some(surface) = ctx.active_detail_surface.as_ref() {
939                            if let Some(surface) = region.map.surfaces.get_mut(&surface.id) {
940                                if let Some(profile_id) = surface.profile {
941                                    return region.map.profiles.get_mut(&profile_id);
942                                }
943                            }
944                        }
945                    }
946                    return Some(&mut region.map);
947                }
948            } else if let Some(surface) = &ctx.editing_surface {
949                if let Some(region) = self.regions.iter_mut().find(|t| t.id == id) {
950                    if let Some(surface) = region.map.surfaces.get_mut(&surface.id) {
951                        if let Some(profile_id) = surface.profile {
952                            return region.map.profiles.get_mut(&profile_id);
953                        }
954                    }
955                }
956                return None;
957            } else if let Some(region) = self.regions.iter_mut().find(|t| t.id == id) {
958                return Some(&mut region.map);
959            }
960            // }
961        } else if ctx.get_map_context() == MapContext::Screen {
962            if let Some(id) = ctx.pc.id() {
963                if let Some(screen) = self.screens.get_mut(&id) {
964                    return Some(&mut screen.map);
965                }
966            }
967        } else if ctx.get_map_context() == MapContext::Character {
968            if let ContentContext::CharacterTemplate(id) = ctx.curr_character {
969                if let Some(character) = self.characters.get_mut(&id) {
970                    return Some(&mut character.map);
971                }
972            }
973        } else if ctx.get_map_context() == MapContext::Item {
974            if let ContentContext::ItemTemplate(id) = ctx.curr_item {
975                if let Some(item) = self.items.get_mut(&id) {
976                    return Some(&mut item.map);
977                }
978            }
979        }
980        None
981    }
982
983    /// Add Screen
984    pub fn add_screen(&mut self, screen: Screen) {
985        self.screens.insert(screen.id, screen);
986    }
987
988    /// Removes the given code from the project.
989    pub fn remove_screen(&mut self, id: &Uuid) {
990        self.screens.shift_remove(id);
991    }
992
993    /// Returns a list of all screens sorted by name.
994    pub fn sorted_screens_list(&self) -> Vec<(Uuid, String)> {
995        let mut entries: Vec<(Uuid, String)> = self
996            .screens
997            .iter()
998            .map(|(uuid, data)| (*uuid, data.name.clone()))
999            .collect();
1000
1001        entries.sort_by(|a, b| a.1.cmp(&b.1));
1002        entries
1003    }
1004
1005    /// Add an asset
1006    pub fn add_asset(&mut self, asset: Asset) {
1007        self.assets.insert(asset.id, asset);
1008    }
1009
1010    /// Removes the given code from the project.
1011    pub fn remove_asset(&mut self, id: &Uuid) {
1012        self.assets.shift_remove(id);
1013    }
1014
1015    /// Returns a list of all assets sorted by name.
1016    pub fn sorted_assets_list(&self) -> Vec<(Uuid, String)> {
1017        let mut entries: Vec<(Uuid, String)> = self
1018            .assets
1019            .iter()
1020            .map(|(uuid, data)| (*uuid, data.name.clone()))
1021            .collect();
1022
1023        entries.sort_by(|a, b| a.1.cmp(&b.1));
1024        entries
1025    }
1026
1027    /// Removes the given tile from the project.
1028    pub fn remove_tile(&mut self, id: &Uuid) {
1029        for tilemap in &mut self.tilemaps {
1030            tilemap.tiles.retain(|t| t.id != *id);
1031        }
1032        self.tiles.shift_remove(id);
1033    }
1034
1035    /// Gets the given tile from the project.
1036    pub fn get_tile(&self, id: &Uuid) -> Option<&Tile> {
1037        for tilemap in &self.tilemaps {
1038            for tile in &tilemap.tiles {
1039                if tile.id == *id {
1040                    return Some(tile);
1041                }
1042            }
1043        }
1044        None
1045    }
1046
1047    /// Gets the given mutable tile from the project.
1048    pub fn get_tile_mut(&mut self, id: &Uuid) -> Option<&mut Tile> {
1049        for tilemap in &mut self.tilemaps {
1050            for tile in &mut tilemap.tiles {
1051                if tile.id == *id {
1052                    return Some(tile);
1053                }
1054            }
1055        }
1056        None
1057    }
1058
1059    /// Extract all tiles from all tilemaps and store them in a hash.
1060    pub fn extract_tiles(&self) -> IndexMap<Uuid, TheRGBATile> {
1061        let mut tiles = IndexMap::default();
1062        for tilemap in &self.tilemaps {
1063            for tile in &tilemap.tiles {
1064                let mut rgba_tile = TheRGBATile::new();
1065                rgba_tile.id = tile.id;
1066                rgba_tile.name.clone_from(&tile.name);
1067                rgba_tile.buffer = tilemap.buffer.extract_sequence(&tile.sequence);
1068                rgba_tile.role = tile.role as u8;
1069                rgba_tile.scale = tile.scale;
1070                rgba_tile.render_mode = tile.render_mode;
1071                rgba_tile.blocking = tile.blocking;
1072                tiles.insert(tile.id, rgba_tile);
1073            }
1074        }
1075        tiles
1076    }
1077
1078    /// Extract all tiles from all tilemaps and store them in a vec.
1079    pub fn extract_tiles_vec(&self) -> Vec<TheRGBATile> {
1080        let mut tiles = vec![];
1081        for tilemap in &self.tilemaps {
1082            for tile in &tilemap.tiles {
1083                let mut rgba_tile = TheRGBATile::new();
1084                rgba_tile.id = tile.id;
1085                rgba_tile.name.clone_from(&tile.name);
1086                rgba_tile.buffer = tilemap.buffer.extract_sequence(&tile.sequence);
1087                rgba_tile.role = tile.role as u8;
1088                tiles.push(rgba_tile);
1089            }
1090        }
1091        tiles
1092    }
1093
1094    /// Extract the given tile from the tilemaps.
1095    pub fn extract_tile(&self, id: &Uuid) -> Option<TheRGBATile> {
1096        for tilemap in &self.tilemaps {
1097            for tile in &tilemap.tiles {
1098                if tile.id == *id {
1099                    let mut rgba_tile = TheRGBATile::new();
1100                    rgba_tile.id = tile.id;
1101                    rgba_tile.name.clone_from(&tile.name);
1102                    rgba_tile.buffer = tilemap.buffer.extract_sequence(&tile.sequence);
1103                    rgba_tile.role = tile.role as u8;
1104                    return Some(rgba_tile);
1105                }
1106            }
1107        }
1108        None
1109    }
1110}