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}