screeps-game-api 0.21.0

WASM bindings to the in-game Screeps API
Documentation
use std::mem::MaybeUninit;

use js_sys::Uint8Array;

use crate::{constants::Terrain, objects::RoomTerrain};

use super::{xy_to_terrain_index, RoomXY, ROOM_AREA};

#[derive(Debug, Clone)]
pub struct LocalRoomTerrain {
    bits: Box<[u8; ROOM_AREA]>,
}

/// A matrix representing the terrain of a room, stored in Rust memory.
///
/// Use [`RoomTerrain`] if data stored in JavaScript memory is preferred.
impl LocalRoomTerrain {
    /// Gets the terrain at the specified position in this room.
    pub fn get_xy(&self, xy: RoomXY) -> Terrain {
        // SAFETY: RoomXY is always a valid coordinate.
        let byte = unsafe { self.bits.get_unchecked(xy_to_terrain_index(xy)) };
        // not using Terrain::from_u8() because `0b11` value, wall+swamp, happens
        // in commonly used server environments (notably the private server default
        // map), and is special-cased in the engine code; we special-case it here
        match byte & 0b11 {
            0b00 => Terrain::Plain,
            0b01 | 0b11 => Terrain::Wall,
            0b10 => Terrain::Swamp,
            // Should be optimized out
            _ => unreachable!("all combinations of 2 bits are covered"),
        }
    }

    /// Creates a `LocalRoomTerrain` from the bytes that correspond to the
    /// room's terrain data.
    ///
    /// This is like the `RoomTerrain` type but performs all operations on data
    /// stored in wasm memory. Each byte in the array corresponds to the value
    /// of the `Terrain` at the given position.
    ///
    /// The bytes are in row-major order - that is they start at the top left,
    /// then move to the top right, and then start at the left of the next row.
    /// This is different from `LocalCostMatrix`, which is column-major.
    pub fn new_from_bits(bits: Box<[u8; ROOM_AREA]>) -> Self {
        Self { bits }
    }
}

impl From<RoomTerrain> for LocalRoomTerrain {
    fn from(terrain: RoomTerrain) -> LocalRoomTerrain {
        // create an uninitialized array of the correct size
        let mut data: Box<[MaybeUninit<u8>; ROOM_AREA]> =
            Box::new([MaybeUninit::uninit(); ROOM_AREA]);
        // create a Uint8Array mapped to the same point in wasm linear memory as our
        // uninitialized boxed array

        // SAFETY: if any allocations happen in rust, this buffer will be detached from
        // wasm memory and no longer writable - we use it immediately then discard it to
        // avoid this
        let js_buffer =
            unsafe { Uint8Array::view_mut_raw(data.as_mut_ptr() as *mut u8, ROOM_AREA) };

        // copy the terrain buffer into the memory backing the Uint8Array - this is the
        // boxed array, so this initializes it
        terrain
            .get_raw_buffer_to_array(&js_buffer)
            .expect("terrain data to copy");
        // data copied - explicitly drop the Uint8Array, so there's no chance it's used
        // again
        drop(js_buffer);
        // we've got the data in our boxed array, change to the needed type
        // SAFETY: `Box` has the same layout for sized types. `MaybeUninit<u8>` has the
        // same layout as `u8`. The arrays are the same size. The `MaybeUninit<u8>` are
        // all initialized because JS wrote to them.
        LocalRoomTerrain::new_from_bits(unsafe {
            std::mem::transmute::<Box<[MaybeUninit<u8>; ROOM_AREA]>, Box<[u8; ROOM_AREA]>>(data)
        })
    }
}