minetestworld/
voxel_manip.rs

1//! Contains a type to more high-level world reading and writing
2
3use std::collections::hash_map::Entry;
4use std::collections::HashMap;
5
6use crate::{MapBlock, MapData, MapDataError, Node, Position};
7type Result<T> = std::result::Result<T, MapDataError>;
8
9struct CacheEntry {
10    mapblock: MapBlock,
11    tainted: bool,
12}
13
14/// In-memory world data cache that allows easy handling of single nodes.
15///
16/// It is an abstraction on top of the MapBlocks the world data consists of.
17/// It allows fast reading from and writing to the world.
18///
19/// All changes to the world have to be committed via [`VoxelManip::commit`].
20/// Before this, they are only present in VoxelManip's local cache and lost after drop.
21///
22/// ⚠️ You want to do a world backup before modifying the map data.
23pub struct VoxelManip {
24    map: MapData,
25    mapblock_cache: HashMap<Position, CacheEntry>,
26}
27
28impl VoxelManip {
29    /// Create a new VoxelManip from a handle to a map data backend
30    pub fn new(map: MapData) -> Self {
31        VoxelManip {
32            map,
33            mapblock_cache: HashMap::new(),
34        }
35    }
36
37    /// Return a cache entry containing the given mapblock
38    async fn get_entry(&mut self, mapblock_pos: Position) -> Result<&mut CacheEntry> {
39        match self.mapblock_cache.entry(mapblock_pos) {
40            Entry::Occupied(e) => Ok(e.into_mut()),
41            Entry::Vacant(e) => {
42                let mapblock = match self.map.get_mapblock(mapblock_pos).await {
43                    Ok(mapblock) => Ok(mapblock),
44                    // If not in the database, create unloaded mapblock
45                    Err(MapDataError::MapBlockNonexistent(_)) => Ok(MapBlock::unloaded()),
46                    Err(e) => Err(e),
47                }?;
48                Ok(e.insert(CacheEntry {
49                    mapblock,
50                    tainted: false,
51                }))
52            }
53        }
54    }
55
56    /// Get a reference to the mapblock at the given block position
57    ///
58    /// If there is no mapblock at this world position,
59    /// a new [unloaded](`MapBlock::unloaded`) mapblock is returned.
60    pub async fn get_mapblock(&mut self, mapblock_pos: Position) -> Result<&MapBlock> {
61        Ok(&self.get_entry(mapblock_pos).await?.mapblock)
62    }
63
64    /// Get the node at the given world position
65    pub async fn get_node(&mut self, node_pos: Position) -> Result<Node> {
66        let (blockpos, nodepos) = node_pos.split_at_block();
67        Ok(self.get_mapblock(blockpos).await?.get_node_at(nodepos))
68    }
69
70    /// Do something with the mapblock at `blockpos` and mark it as modified
71    async fn modify_mapblock(
72        &mut self,
73        blockpos: Position,
74        op: impl FnOnce(&mut MapBlock),
75    ) -> Result<()> {
76        let entry = &mut self.get_entry(blockpos).await?;
77        op(&mut entry.mapblock);
78        entry.tainted = true;
79        Ok(())
80    }
81
82    /// Set a voxel in VoxelManip's cache
83    ///
84    /// ⚠️ The change will be present locally only. To modify the map,
85    /// the change has to be written back via [`VoxelManip::commit`].
86    pub async fn set_node(&mut self, node_pos: Position, node: Node) -> Result<()> {
87        let (blockpos, nodepos) = node_pos.split_at_block();
88        self.modify_mapblock(blockpos, |mapblock| {
89            let content_id = mapblock.get_or_create_content_id(&node.param0);
90            mapblock.set_content(nodepos, content_id);
91            mapblock.set_param1(nodepos, node.param1);
92            mapblock.set_param2(nodepos, node.param2);
93        })
94        .await
95    }
96
97    /// Sets the content string at this world position
98    ///
99    /// `content` has to be the unique [itemstring](https://wiki.minetest.net/Itemstrings).
100    /// The use of aliases is not possible, because it would require a Lua runtime
101    /// loading all mods.
102    ///
103    /// ```ignore
104    /// vm.set_content(Position::new(8,9,10), b"default:stone").await?;
105    /// ```
106    ///
107    /// ⚠️ Until the change is [commited](`VoxelManip::commit`),
108    /// the node will only be changed in the cache.
109    pub async fn set_content(&mut self, node_pos: Position, content: &[u8]) -> Result<()> {
110        let (blockpos, nodepos) = node_pos.split_at_block();
111        self.modify_mapblock(blockpos, |mapblock| {
112            let content_id = mapblock.get_or_create_content_id(content);
113            mapblock.set_content(nodepos, content_id);
114        })
115        .await
116    }
117
118    /// Sets the lighting parameter at this world position
119    ///
120    /// ⚠️ Until the change is [commited](`VoxelManip::commit`),
121    /// the node will only be changed in the cache.
122    pub async fn set_param1(&mut self, node_pos: Position, param1: u8) -> Result<()> {
123        let (blockpos, nodepos) = node_pos.split_at_block();
124        self.modify_mapblock(blockpos, |mapblock| {
125            mapblock.set_param1(nodepos, param1);
126        })
127        .await
128    }
129
130    /// Sets the param2 of the node at this world position
131    ///
132    /// ⚠️ Until the change is [commited](`VoxelManip::commit`),
133    /// the node will only be changed in the cache.
134    pub async fn set_param2(&mut self, node_pos: Position, param2: u8) -> Result<()> {
135        let (blockpos, nodepos) = node_pos.split_at_block();
136        self.modify_mapblock(blockpos, |mapblock| {
137            mapblock.set_param2(nodepos, param2);
138        })
139        .await
140    }
141
142    /// Returns true if this world position is cached
143    pub fn is_in_cache(&self, node_pos: Position) -> bool {
144        let blockpos = node_pos.mapblock_at();
145        self.mapblock_cache.contains_key(&blockpos)
146    }
147
148    /// Ensures that this world position is in the cache
149    pub async fn visit(&mut self, node_pos: Position) -> Result<()> {
150        let blockpos = node_pos.mapblock_at();
151        self.get_entry(blockpos).await?;
152        Ok(())
153    }
154
155    /// Apply all changes made to the map
156    ///
157    /// Without this, all changes made with [`VoxelManip::set_node`], [`VoxelManip::set_content`],
158    /// [`VoxelManip::set_param1`], and [`VoxelManip::set_param2`] are lost when this
159    /// instance is dropped.
160    pub async fn commit(&mut self) -> Result<()> {
161        // Write modified mapblocks back into the map data
162        for (&pos, cache_entry) in self.mapblock_cache.iter_mut() {
163            if cache_entry.tainted {
164                self.map.set_mapblock(pos, &cache_entry.mapblock).await?;
165                cache_entry.tainted = false;
166            }
167        }
168
169        Ok(())
170    }
171}