Skip to main content

wow_adt/chunks/mcnk/
mcsh.rs

1//! MCSH chunk - Shadow map (64×64 1-bit map, Vanilla+).
2//!
3//! Contains pre-baked shadow data for terrain. Each bit represents one texel
4//! in the 64×64 shadow texture.
5//!
6//! Format:
7//! - 64 rows × 8 bytes per row = 512 bytes total
8//! - LSB-first bit ordering within bytes
9//! - Bit set = shadowed texel
10//!
11//! This chunk is optional and only present if McnkHeader.flags.has_mcsh() is true.
12//!
13//! Reference: <https://wowdev.wiki/ADT/v18#MCSH_sub-chunk>
14
15use binrw::{BinRead, BinWrite};
16
17/// MCSH chunk - Shadow map (64×64 1-bit map, Vanilla+).
18///
19/// Contains pre-baked shadow data for terrain. Each bit represents one texel
20/// in the 64×64 shadow texture.
21///
22/// # Format
23///
24/// - 64 rows × 8 bytes per row = 512 bytes total
25/// - LSB-first bit ordering within bytes
26/// - Bit set = shadowed texel
27///
28/// This chunk is optional and only present if McnkFlags.has_mcsh() is true.
29///
30/// ## Version Support
31///
32/// - **Vanilla (1.12.1)**: ✅ Introduced
33/// - **TBC (2.4.3)**: ✅ Present
34/// - **WotLK (3.3.5a)**: ✅ Present
35/// - **Cataclysm (4.3.4)**: ✅ Present
36/// - **MoP (5.4.8)**: ✅ Present
37///
38/// # Binary Layout
39///
40/// ```text
41/// Offset | Size | Field       | Description
42/// -------|------|-------------|----------------------------------
43/// 0x000  | 512  | shadow_map  | 64×64 1-bit shadow map
44/// ```
45///
46/// Reference: <https://wowdev.wiki/ADT/v18#MCSH_sub-chunk>
47#[derive(Debug, Clone, BinRead, BinWrite)]
48#[brw(little)]
49pub struct McshChunk {
50    /// Shadow map data (512 bytes = 64 rows * 8 bytes/row)
51    ///
52    /// Each row is 8 bytes (64 bits) representing one row of the 64×64 map.
53    /// Bit ordering: LSB-first within each byte.
54    #[br(count = 512)]
55    pub shadow_map: Vec<u8>,
56}
57
58impl Default for McshChunk {
59    fn default() -> Self {
60        Self {
61            shadow_map: vec![0; 512], // All unshadowed
62        }
63    }
64}
65
66impl McshChunk {
67    /// Shadow map resolution (64×64 texels).
68    pub const RESOLUTION: usize = 64;
69
70    /// Size in bytes (512 = 64 * 8).
71    pub const SIZE_BYTES: usize = 512;
72
73    /// Get shadow value at texel position.
74    ///
75    /// # Arguments
76    ///
77    /// * `x` - Column index (0-63)
78    /// * `y` - Row index (0-63)
79    ///
80    /// # Returns
81    ///
82    /// `true` if texel is shadowed, `false` if lit or indices out of bounds
83    pub fn is_shadowed(&self, x: usize, y: usize) -> bool {
84        if x >= Self::RESOLUTION || y >= Self::RESOLUTION {
85            return false;
86        }
87
88        let byte_index = y * 8 + (x / 8);
89        let bit_index = x % 8;
90
91        if let Some(&byte) = self.shadow_map.get(byte_index) {
92            (byte >> bit_index) & 1 != 0
93        } else {
94            false
95        }
96    }
97
98    /// Set shadow value at texel position.
99    ///
100    /// # Arguments
101    ///
102    /// * `x` - Column index (0-63)
103    /// * `y` - Row index (0-63)
104    /// * `shadowed` - `true` to mark texel as shadowed
105    pub fn set_shadow(&mut self, x: usize, y: usize, shadowed: bool) {
106        if x >= Self::RESOLUTION || y >= Self::RESOLUTION {
107            return;
108        }
109
110        let byte_index = y * 8 + (x / 8);
111        let bit_index = x % 8;
112
113        if let Some(byte) = self.shadow_map.get_mut(byte_index) {
114            if shadowed {
115                *byte |= 1 << bit_index;
116            } else {
117                *byte &= !(1 << bit_index);
118            }
119        }
120    }
121
122    /// Count number of shadowed texels.
123    pub fn shadowed_count(&self) -> usize {
124        self.shadow_map
125            .iter()
126            .map(|&byte| byte.count_ones() as usize)
127            .sum()
128    }
129
130    /// Get percentage of shadowed texels (0.0 to 1.0).
131    pub fn shadow_ratio(&self) -> f32 {
132        let total_texels = Self::RESOLUTION * Self::RESOLUTION;
133        self.shadowed_count() as f32 / total_texels as f32
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use std::io::Cursor;
141
142    #[test]
143    fn test_mcsh_chunk_size() {
144        let chunk = McshChunk::default();
145        assert_eq!(chunk.shadow_map.len(), 512);
146        assert_eq!(McshChunk::SIZE_BYTES, 512);
147        assert_eq!(McshChunk::RESOLUTION, 64);
148    }
149
150    #[test]
151    fn test_mcsh_parse_512_bytes() {
152        let data = vec![0u8; 512];
153        let mut cursor = Cursor::new(data);
154        let chunk = McshChunk::read_le(&mut cursor).unwrap();
155
156        assert_eq!(chunk.shadow_map.len(), 512);
157        assert_eq!(chunk.shadowed_count(), 0);
158    }
159
160    #[test]
161    fn test_mcsh_default_all_unshadowed() {
162        let chunk = McshChunk::default();
163
164        for y in 0..64 {
165            for x in 0..64 {
166                assert!(!chunk.is_shadowed(x, y));
167            }
168        }
169
170        assert_eq!(chunk.shadowed_count(), 0);
171        assert_eq!(chunk.shadow_ratio(), 0.0);
172    }
173
174    #[test]
175    fn test_mcsh_shadow_bit_access() {
176        let mut chunk = McshChunk::default();
177
178        // Set some shadow bits
179        chunk.set_shadow(0, 0, true);
180        chunk.set_shadow(7, 0, true);
181        chunk.set_shadow(31, 15, true);
182        chunk.set_shadow(63, 63, true);
183
184        assert!(chunk.is_shadowed(0, 0));
185        assert!(chunk.is_shadowed(7, 0));
186        assert!(chunk.is_shadowed(31, 15));
187        assert!(chunk.is_shadowed(63, 63));
188
189        assert!(!chunk.is_shadowed(1, 0));
190        assert!(!chunk.is_shadowed(0, 1));
191
192        // Clear a bit
193        chunk.set_shadow(0, 0, false);
194        assert!(!chunk.is_shadowed(0, 0));
195    }
196
197    #[test]
198    fn test_mcsh_lsb_first_bit_ordering() {
199        let mut chunk = McshChunk::default();
200
201        // Set first 8 bits in row 0 (byte 0)
202        for x in 0..8 {
203            chunk.set_shadow(x, 0, true);
204        }
205
206        // Verify byte 0 is 0xFF (all bits set, LSB-first)
207        assert_eq!(chunk.shadow_map[0], 0xFF);
208
209        // Set alternating bits in row 1 (byte 8)
210        chunk.set_shadow(0, 1, true); // bit 0
211        chunk.set_shadow(2, 1, true); // bit 2
212        chunk.set_shadow(4, 1, true); // bit 4
213        chunk.set_shadow(6, 1, true); // bit 6
214
215        // Verify byte 8 is 0b01010101 = 0x55
216        assert_eq!(chunk.shadow_map[8], 0b0101_0101);
217
218        // Verify individual bit reads
219        assert!(chunk.is_shadowed(0, 1));
220        assert!(!chunk.is_shadowed(1, 1));
221        assert!(chunk.is_shadowed(2, 1));
222        assert!(!chunk.is_shadowed(3, 1));
223    }
224
225    #[test]
226    fn test_mcsh_bounds_checking() {
227        let mut chunk = McshChunk::default();
228
229        // Out of bounds get
230        assert!(!chunk.is_shadowed(64, 0));
231        assert!(!chunk.is_shadowed(0, 64));
232        assert!(!chunk.is_shadowed(100, 100));
233
234        // Out of bounds set (should not panic)
235        chunk.set_shadow(64, 0, true);
236        chunk.set_shadow(0, 64, true);
237        chunk.set_shadow(100, 100, true);
238
239        // Verify no state changed
240        assert_eq!(chunk.shadowed_count(), 0);
241    }
242
243    #[test]
244    fn test_mcsh_shadow_count() {
245        let mut chunk = McshChunk::default();
246
247        assert_eq!(chunk.shadowed_count(), 0);
248
249        // Set 10 random shadow bits
250        chunk.set_shadow(0, 0, true);
251        chunk.set_shadow(10, 5, true);
252        chunk.set_shadow(20, 10, true);
253        chunk.set_shadow(30, 15, true);
254        chunk.set_shadow(40, 20, true);
255        chunk.set_shadow(50, 25, true);
256        chunk.set_shadow(15, 30, true);
257        chunk.set_shadow(25, 35, true);
258        chunk.set_shadow(35, 40, true);
259        chunk.set_shadow(45, 45, true);
260
261        assert_eq!(chunk.shadowed_count(), 10);
262    }
263
264    #[test]
265    fn test_mcsh_shadow_ratio() {
266        let mut chunk = McshChunk::default();
267
268        // No shadows
269        assert_eq!(chunk.shadow_ratio(), 0.0);
270
271        // Set all bits in first row (64 bits)
272        for x in 0..64 {
273            chunk.set_shadow(x, 0, true);
274        }
275
276        let expected_ratio = 64.0 / 4096.0; // 64 out of 64×64
277        assert!((chunk.shadow_ratio() - expected_ratio).abs() < 0.0001);
278
279        // Set all bits
280        for y in 0..64 {
281            for x in 0..64 {
282                chunk.set_shadow(x, y, true);
283            }
284        }
285
286        assert_eq!(chunk.shadow_ratio(), 1.0);
287        assert_eq!(chunk.shadowed_count(), 4096);
288    }
289
290    #[test]
291    fn test_mcsh_round_trip() {
292        let mut original = McshChunk::default();
293
294        // Create a pattern: diagonal shadow
295        for i in 0..64 {
296            original.set_shadow(i, i, true);
297        }
298
299        assert_eq!(original.shadowed_count(), 64);
300
301        // Serialize
302        let mut buffer = Cursor::new(Vec::new());
303        original.write_le(&mut buffer).unwrap();
304
305        let data = buffer.into_inner();
306        assert_eq!(data.len(), 512);
307
308        // Deserialize
309        let mut cursor = Cursor::new(data);
310        let parsed = McshChunk::read_le(&mut cursor).unwrap();
311
312        // Verify
313        assert_eq!(parsed.shadowed_count(), 64);
314        for i in 0..64 {
315            assert!(parsed.is_shadowed(i, i));
316        }
317    }
318
319    #[test]
320    fn test_mcsh_byte_indexing() {
321        let mut chunk = McshChunk::default();
322
323        // Row 0, column 0-7 -> byte 0
324        chunk.set_shadow(0, 0, true);
325        assert_eq!(chunk.shadow_map[0], 0b0000_0001);
326
327        chunk.set_shadow(7, 0, true);
328        assert_eq!(chunk.shadow_map[0], 0b1000_0001);
329
330        // Row 0, column 8-15 -> byte 1
331        chunk.set_shadow(8, 0, true);
332        assert_eq!(chunk.shadow_map[1], 0b0000_0001);
333
334        // Row 1, column 0-7 -> byte 8
335        chunk.set_shadow(0, 1, true);
336        assert_eq!(chunk.shadow_map[8], 0b0000_0001);
337
338        // Row 63, column 56-63 -> byte 511
339        chunk.set_shadow(63, 63, true);
340        assert_eq!(chunk.shadow_map[511], 0b1000_0000);
341    }
342
343    #[test]
344    fn test_mcsh_all_bytes_used() {
345        let mut chunk = McshChunk::default();
346
347        // Set first bit of every byte
348        for byte_index in 0..512 {
349            let y = byte_index / 8;
350            let x = (byte_index % 8) * 8;
351            chunk.set_shadow(x, y, true);
352        }
353
354        // Verify all 512 bytes have bit 0 set
355        for byte_index in 0..512 {
356            assert_eq!(chunk.shadow_map[byte_index] & 1, 1);
357        }
358
359        assert_eq!(chunk.shadowed_count(), 512);
360    }
361
362    #[test]
363    fn test_mcsh_full_coverage() {
364        let mut chunk = McshChunk::default();
365
366        // Set every texel
367        for y in 0..64 {
368            for x in 0..64 {
369                chunk.set_shadow(x, y, true);
370            }
371        }
372
373        // Verify all bytes are 0xFF
374        for &byte in &chunk.shadow_map {
375            assert_eq!(byte, 0xFF);
376        }
377
378        assert_eq!(chunk.shadowed_count(), 4096);
379        assert_eq!(chunk.shadow_ratio(), 1.0);
380    }
381}