simple_anvil/chunk.rs
1use nbt::{Blob, Value};
2
3use crate::{block::Block, region::Region};
4
5use std::{cmp, collections::HashMap};
6
7/// A simple representation of a Minecraft Chunk
8#[derive(Clone)]
9pub struct Chunk {
10    /// All of the chunk data
11    pub data: Box<Blob>,
12    /// The region x
13    pub x: u32,
14    /// The region z
15    pub z: u32,
16}
17
18impl Chunk {
19    
20    /// Returns the chunk at an x,z coordinate within a Region.
21    /// 
22    /// # Arguments
23    /// 
24    /// * `region` - The Region from which to get the Chunk
25    /// * `chunk_x` - The x coordinate within the Region of the Chunk
26    /// * `chunk_z` - The z coordinate within the Region of the Chunk
27    pub fn from_region(region: &Region, chunk_x: u32, chunk_z: u32) -> Option<Chunk> {
28        match region.chunk_data(chunk_x, chunk_z) {
29            Some(data) => {
30                return Some(Chunk{ data, x: chunk_x, z: chunk_z });
31            }
32            None => None,
33        }
34    }
35
36    /// Returns a string representing the current generation state of the Chunk. 'full' is completely generated.
37    /// 
38    /// # Examples
39    /// 
40    /// ```rust,no_run
41    /// use simple_anvil::region::Region;
42    /// let region = Region::from_file("r.0.0.mca");
43    /// let chunk = region.get_chunk(0, 0).unwrap();
44    /// if chunk.get_status() == "full" {
45    ///     println!("Fully Generated!");
46    /// }
47    /// ```
48    pub fn get_status(&self) -> &String {
49        if let Value::String(s) = self.data.get("Status").unwrap() {
50            s
51        } else {
52            panic!("Value should be a string?")
53        }
54    }
55
56    /// Returns an i64 (equivalent of Java long) of the last tick at which the chunk updated.
57    /// 
58    /// # Examples
59    /// 
60    /// ```rust,no_run
61    /// use simple_anvil::region::Region;
62    /// let region = Region::from_file("r.0.0.mca");
63    /// let chunk = region.get_chunk(0, 0).unwrap();
64    /// println!("{}", chunk.get_last_update());
65    /// ```
66    pub fn get_last_update(&self) -> &i64 {
67        if let Value::Long(l) = self.data.get("LastUpdate").unwrap() {
68            l
69        } else {
70            panic!("Value should be a i64")
71        }
72    }
73
74    /// Returns a heightmap of the Chunk. If the Chunk is not fully generated then a None is returned.
75    /// 
76    /// # Arguments
77    /// 
78    /// * `ignore_water` - Determines which heightmap to return, if true then a heightmap that does not take into account the water is returned (OCEAN_FLOOR), if false then the water is accounted for (WORLD_SURFACE).
79    /// 
80    ///  # Examples
81    /// 
82    /// ```rust,no_run
83    /// use simple_anvil::region::Region;
84    /// let region = Region::from_file("r.0.0.mca");
85    /// let chunk = region.get_chunk(0, 0).unwrap();
86    /// let heightmap = chunk.get_heightmap(false);
87    /// ```
88    pub fn get_heightmap(&self, ignore_water: bool) -> Option<Vec<i32>> {
89        if self.get_status() == "full" {
90            let height_maps = if let Value::Compound(hm) = self.data.get("Heightmaps").unwrap() {
91                hm
92            } else {
93                panic!()
94            };
95
96            let map = if ignore_water {
97                "OCEAN_FLOOR"
98            } else {
99                "WORLD_SURFACE"
100            };
101
102            let surface = if let Value::LongArray(la) = height_maps.get(map).unwrap() {
103                la
104            } else {
105                panic!("no ocean?")
106            };
107
108            let surface_binary: Vec<String> = surface.iter().map(|n| format!("{:b}", n)).map(|n| "0".repeat(63 - n.len()) + &n).collect();
109            let mut all = Vec::new();
110            // let mut hmm = Vec::new();
111
112            for num in surface_binary {
113                let num_chars = num.chars().collect::<Vec<_>>();
114                let mut sub_nums = num_chars.chunks(9).collect::<Vec<&[char]>>();
115                sub_nums.reverse();
116                for num in sub_nums {
117                    let test = num.iter().collect::<String>();
118                    if test != "000000000" {
119                        all.push(test.clone());
120                    }
121                }
122            }
123
124            let mut heights = Vec::new();
125
126            for num in all {
127                let n = usize::from_str_radix(num.as_str(), 2).unwrap();
128                heights.push(n as i32 - 64 - 1);
129            }
130
131            return Some(heights);
132        } else {
133            None
134        }
135    }
136
137    /// Returns a vertical section of a Chunk
138    /// 
139    /// # Arguments
140    /// 
141    /// * `y` - The y index of the section.
142    fn get_section(&self, y: i8) -> Option<HashMap<String, Value>> {
143        if y < -4 || y > 19 {
144            panic!("Y value out of range")
145        }
146        let sections = if let Value::List(s) = self.data.get("sections").unwrap() {
147            s
148        } else {
149            panic!("Value should be a list?")
150        };
151
152        for section in sections {
153            let section = if let Value::Compound(s) = section {
154                s
155            } else {
156                panic!("should be a compound")
157            };
158            let section_y = if let Value::Byte(sec_y) = section.get("Y").unwrap() {
159                sec_y
160            } else {
161                panic!("Failed to get y")
162            };
163            if *section_y == y {
164                let cloned = section.clone();
165                return Some(cloned);
166            }
167        }
168        None
169    }
170
171    /// Returns the String representation of the biome for a Chunk. Chunks can have different biomes at different vertical sections so use a heightmap to determine the top section if you only want the surface biome.
172    /// 
173    /// # Arguments
174    /// 
175    /// * `y` - The y section of the chunk to get the biome of.
176    /// 
177    /// # Examples
178    /// 
179    /// ```rust,no_run
180    /// use simple_anvil::region::Region;
181    /// let region = Region::from_file("r.0.0.mca");
182    /// let chunk = region.get_chunk(0, 0).unwrap();
183    /// let heightmap = chunk.get_heightmap(false);
184    /// let y = if let Some(heights) = heightmap {
185    ///     heights.get(0).unwrap()
186    /// } else {
187    ///     panic!("Chunk not fully generated");
188    /// }
189    /// let section_y = ((y + 64) / 16 - 4) as i8
190    /// let biome = chunk.get_biome(section_y);
191    /// ```
192    /// 
193    /// ```rust,no_run
194    /// use simple_anvil::region::Region;
195    /// let region = Region::from_file("r.0.0.mca");
196    /// let chunk = region.get_chunk(0, 0).unwrap();
197    /// let biome = chunk.get_biome(-3);
198    /// ```
199    pub fn get_biome(&self, y: i32) -> String {
200        let sections = if let Value::List(s) = self.data.get("sections").unwrap() {
201            s
202        } else {
203            panic!("Value should be a list?")
204        };
205        for section in sections {
206            let section = if let Value::Compound(s) = section {
207                s
208            } else {
209                panic!("Should be a compound?")
210            };
211            let current_y = if let Value::Byte(val) = section.get("Y").unwrap() {
212                val
213            } else {
214                panic!("invalid height found")
215            };
216            if current_y == &(((y + 64) / 16 - 4) as i8) {
217                let biomes = if let Value::Compound(c) = section.get("biomes").unwrap() {
218                    c
219                } else {
220                    panic!("biomes not found")
221                };
222                let pallete = if let Value::List(l) = biomes.get("palette").unwrap() {
223                    l
224                } else {
225                    panic!("pallete not found")
226                };
227                let biome = if let Value::String(s) = &pallete[0] {
228                    s
229                } else {
230                    panic!("failed to get string")
231                };
232                return biome.to_string();
233            }
234            
235        };
236        return String::from("minecraft:ocean")
237    }
238
239    /// Returns the block at a particular x, y, z coordinate within a chunk. x and z should be the coordinates within the Chunk (0-15).
240    /// 
241    /// # Examples
242    /// 
243    /// ```rust,no_run
244    /// use simple_anvil::region::Region;
245    /// let region = Region::from_file("r.0.0.mca");
246    /// let chunk = region.get_chunk(0, 0).unwrap();
247    /// let block = chunk.get_block(5, -12, 11);
248    /// println!("{}", block.id);
249    /// ```
250    pub fn get_block(&self, x: i32, mut y: i32, z: i32) -> Block {
251        let section = self.get_section(((y + 64) / 16 - 4) as i8);
252        if section == None {
253            return Block::from_name(String::from("minecraft:air"), Some((self.x as i32 * 32 + x, y, self.z as i32 * 32 + z)), None);
254        }
255        let section = section.unwrap();
256        y = y.rem_euclid(16);
257        let block_states = if let Some(Value::Compound(bs)) = section.get("block_states") {
258            Some(bs)
259        } else {
260            None
261        };
262        if block_states == None {
263            return Block::from_name(String::from("minecraft:air"), Some((self.x as i32 * 32 + x, y, self.z as i32 * 32 + z)), None);
264        }
265
266        let palette = if let Value::List(p) = block_states.unwrap().get("palette").unwrap() {
267            p
268        } else {
269            panic!("Palette should be a list")
270        };
271
272        match block_states {
273            Some(bs) => {
274                let bits = cmp::max(self.bit_length(palette.len() - 1), 4);
275                let index = y * 16 * 16 + z * 16 + x;
276                match bs.get("data") {
277                    Some(data) => {
278                        let states = if let Value::LongArray(la) = data {
279                            la
280                        } else {
281                            panic!("something here")
282                        };
283                        let state = index as usize / (64 / bits as usize);
284                        let data = states[state];
285                        let mut d = 0;
286                        let mut modified = false;
287                        if data < 0 {
288                            d = data as u64;
289                            modified = true;
290                        }
291                        let shifted_data = (if modified { d as usize } else { data as usize }) >> (index as usize % (64 / bits as usize) * bits as usize);
292                        let palette_id = shifted_data & (2u32.pow(bits) - 1) as usize;
293                        let block = &palette[palette_id];
294                        // let props = 
295                        let props = if let Value::Compound(c) = block {
296                            match c.get("Properties") {
297                                Some(p_val) => {
298                                    let properties = if let Value::Compound(p) = p_val {
299                                        p
300                                    } else {
301                                        panic!("Properties should be a compound")
302                                    };
303                                    Some(properties.iter().map(|f| (f.0.to_owned(), if let Value::String(s) = f.1 {
304                                        s.to_owned()
305                                    } else {
306                                        panic!("Should be a string?")
307                                    })).collect::<Vec<_>>())
308  
309                                },
310                                None => None,
311                            }
312                        } else {
313                            panic!("block should be a compound")
314                        };
315                        return Block::from_palette(block, Some((self.x as i32 * 32 + x, y, self.z as i32 * 32 + z)), props);
316                    },
317                    None => return Block::from_name(String::from("minecraft:air"), Some((self.x as i32 * 32 + x, y, self.z as i32 * 32 + z)), None)
318                } 
319            },
320            None => {
321                return Block::from_name(String::from("minecraft:air"), Some((self.x as i32 * 32 + x, y, self.z as i32 * 32 + z)), None);
322            },
323        }
324        
325    }
326
327    /// Returns the bitlength of a usize value
328    fn bit_length(&self, num: usize) -> u32 {
329        // The number of bits that the number consists of, this is an integer and we don't care about signs or leading 0's
330        // 0001 and 1 have the same return value
331        // I think the lowest number that could come in is -1?
332        // usize is always returned from the len function so I think that it will only be usize?
333        if num == 0 {
334            return 0;
335        }
336        // Convert the number to a string version of the binary representation
337        // Get the number of leading 0's
338        let _leading = num.leading_zeros();
339        // Place the number into binary
340        let s_num = format!("{:b}", num);
341        // Remove leading 0's
342        // let s = &s_num[leading as usize..];
343        // Return the length
344        // Leading zeros appear to be removed when changed to bits
345        return s_num.len() as u32;
346    }
347}