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
51impl From<RoomTerrain> for LocalRoomTerrain {
52    fn from(terrain: RoomTerrain) -> LocalRoomTerrain {
53        // create an uninitialized array of the correct size
54        let mut data: Box<[MaybeUninit<u8>; ROOM_AREA]> =
55            Box::new([MaybeUninit::uninit(); ROOM_AREA]);
56        // create a Uint8Array mapped to the same point in wasm linear memory as our
57        // uninitialized boxed array
58
59        // SAFETY: if any allocations happen in rust, this buffer will be detached from
60        // wasm memory and no longer writable - we use it immediately then discard it to
61        // avoid this
62        let js_buffer =
63            unsafe { Uint8Array::view_mut_raw(data.as_mut_ptr() as *mut u8, ROOM_AREA) };
64
65        // copy the terrain buffer into the memory backing the Uint8Array - this is the
66        // boxed array, so this initializes it
67        terrain
68            .get_raw_buffer_to_array(&js_buffer)
69            .expect("terrain data to copy");
70        // data copied - explicitly drop the Uint8Array, so there's no chance it's used
71        // again
72        drop(js_buffer);
73        // we've got the data in our boxed array, change to the needed type
74        // SAFETY: `Box` has the same layout for sized types. `MaybeUninit<u8>` has the
75        // same layout as `u8`. The arrays are the same size. The `MaybeUninit<u8>` are
76        // all initialized because JS wrote to them.
77        LocalRoomTerrain::new_from_bits(unsafe {
78            std::mem::transmute::<Box<[MaybeUninit<u8>; ROOM_AREA]>, Box<[u8; ROOM_AREA]>>(data)
79        })
80    }
81}