screeps/local/
terrain.rs

1use std::mem::MaybeUninit;
2
3use js_sys::Uint8Array;
4
5use crate::{
6    constants::{Terrain, ROOM_AREA},
7    objects::RoomTerrain,
8};
9
10use super::RoomXY;
11
12#[derive(Debug, Clone)]
13pub struct LocalRoomTerrain {
14    bits: Box<[u8; ROOM_AREA]>,
15}
16
17/// A matrix representing the terrain of a room, stored in Rust memory.
18///
19/// Use [`RoomTerrain`] if data stored in JavaScript memory is preferred.
20impl LocalRoomTerrain {
21    /// Gets the terrain at the specified position in this room.
22    pub fn get_xy(&self, xy: RoomXY) -> Terrain {
23        let byte = self.bits[xy.y][xy.x];
24        // not using Terrain::from_u8() because `0b11` value, wall+swamp, happens
25        // in commonly used server environments (notably the private server default
26        // map), and is special-cased in the engine code; we special-case it here
27        match byte & 0b11 {
28            0b00 => Terrain::Plain,
29            0b01 | 0b11 => Terrain::Wall,
30            0b10 => Terrain::Swamp,
31            // Should be optimized out
32            _ => unreachable!("all combinations of 2 bits are covered"),
33        }
34    }
35
36    /// Creates a `LocalRoomTerrain` from the bytes that correspond to the
37    /// room's terrain data.
38    ///
39    /// This is like the `RoomTerrain` type but performs all operations on data
40    /// stored in wasm memory. Each byte in the array corresponds to the value
41    /// of the `Terrain` at the given position.
42    ///
43    /// The bytes are in row-major order - that is they start at the top left,
44    /// then move to the top right, and then start at the left of the next row.
45    /// This is different from `LocalCostMatrix`, which is column-major.
46    pub fn new_from_bits(bits: Box<[u8; ROOM_AREA]>) -> Self {
47        Self { bits }
48    }
49
50    /// Gets a slice of the underlying bytes that comprise the room's terrain
51    /// data.
52    ///
53    /// The bytes are in row-major order - that is they start at the top left,
54    /// then move to the top right, and then start at the left of the next row.
55    /// This is different from `LocalCostMatrix`, which is column-major.
56    pub fn get_bits(&self) -> &[u8; ROOM_AREA] {
57        &self.bits
58    }
59}
60
61impl From<RoomTerrain> for LocalRoomTerrain {
62    fn from(terrain: RoomTerrain) -> LocalRoomTerrain {
63        // create an uninitialized array of the correct size
64        let mut data: Box<[MaybeUninit<u8>; ROOM_AREA]> =
65            Box::new([MaybeUninit::uninit(); ROOM_AREA]);
66        // create a Uint8Array mapped to the same point in wasm linear memory as our
67        // uninitialized boxed array
68
69        // SAFETY: if any allocations happen in rust, this buffer will be detached from
70        // wasm memory and no longer writable - we use it immediately then discard it to
71        // avoid this
72        let js_buffer =
73            unsafe { Uint8Array::view_mut_raw(data.as_mut_ptr() as *mut u8, ROOM_AREA) };
74
75        // copy the terrain buffer into the memory backing the Uint8Array - this is the
76        // boxed array, so this initializes it
77        terrain
78            .get_raw_buffer_to_array(&js_buffer)
79            .expect("terrain data to copy");
80        // data copied - explicitly drop the Uint8Array, so there's no chance it's used
81        // again
82        drop(js_buffer);
83        // we've got the data in our boxed array, change to the needed type
84        // SAFETY: `Box` has the same layout for sized types. `MaybeUninit<u8>` has the
85        // same layout as `u8`. The arrays are the same size. The `MaybeUninit<u8>` are
86        // all initialized because JS wrote to them.
87        LocalRoomTerrain::new_from_bits(unsafe {
88            std::mem::transmute::<Box<[MaybeUninit<u8>; ROOM_AREA]>, Box<[u8; ROOM_AREA]>>(data)
89        })
90    }
91}
92
93#[cfg(test)]
94mod test {
95    use super::*;
96    use crate::constants::{ROOM_AREA, ROOM_SIZE};
97
98    #[test]
99    pub fn addresses_data_in_row_major_order() {
100        // Initialize terrain to be all plains
101        let mut raw_terrain_data = Box::new([0; ROOM_AREA]);
102
103        // Adjust (1, 0) to be a swamp; in row-major order this is the second element
104        // (index 1) in the array; in column-major order this is the 51st
105        // element (index 50) in the array.
106        raw_terrain_data[1] = 2; // Terrain::Swamp has the numeric representation 2
107
108        // Construct the local terrain object
109        let terrain = LocalRoomTerrain::new_from_bits(raw_terrain_data);
110
111        // Pull the terrain for location (1, 0); if it comes out as a Swamp, then we
112        // know the get_xy function pulls data in row-major order; if it comes
113        // out as a Plain, then we know that it pulls in column-major order.
114        let xy = unsafe { RoomXY::unchecked_new(1, 0) };
115        let tile_type = terrain.get_xy(xy);
116        assert_eq!(Terrain::Swamp, tile_type);
117    }
118
119    #[test]
120    pub fn get_bits_returns_a_byte_array_that_can_reconstitute_the_local_terrain() {
121        // Initialize terrain to be all plains
122        let mut raw_terrain_data = Box::new([0; ROOM_AREA]);
123
124        // Adjust terrain to be heterogeneous
125        for i in 0..ROOM_AREA {
126            // Safety: mod 3 will always be a valid u8
127            let tile_type: u8 = (i % 3) as u8; // Range: 0, 1, 2 -> Plains, Wall, Swamp
128            raw_terrain_data[i] = tile_type;
129        }
130
131        // Construct the local terrain object
132        let terrain = LocalRoomTerrain::new_from_bits(raw_terrain_data);
133
134        // Grab the bits
135        let bits = *terrain.get_bits();
136
137        // Build the new terrain from the copied bits
138        let new_terrain = LocalRoomTerrain::new_from_bits(Box::new(bits));
139
140        // Iterate over all room positions and verify that they match in both terrain
141        // objects
142        for x in 0..ROOM_SIZE {
143            for y in 0..ROOM_SIZE {
144                // Safety: x and y are both explicitly restricted to room size
145                let xy = unsafe { RoomXY::unchecked_new(x, y) };
146                assert_eq!(terrain.get_xy(xy), new_terrain.get_xy(xy));
147            }
148        }
149    }
150}