screeps/game/
map.rs

1//! Game map related functionality.
2//!
3//! [Screeps documentation](https://docs.screeps.com/api/#Game-map)
4use enum_iterator::Sequence;
5use js_sys::{Array, JsString, Object};
6use num_traits::*;
7use serde::{Deserialize, Serialize};
8use wasm_bindgen::prelude::*;
9
10use crate::{
11    constants::{Direction, ErrorCode, ExitDirection},
12    local::RoomName,
13    objects::RoomTerrain,
14    prelude::*,
15};
16
17#[wasm_bindgen]
18extern "C" {
19    #[wasm_bindgen(js_name = "map")]
20    type Map;
21
22    #[wasm_bindgen(js_namespace = ["Game"], js_class = "map", static_method_of = Map, js_name = describeExits)]
23    fn describe_exits(room_name: &JsString) -> Object;
24
25    #[wasm_bindgen(js_namespace = ["Game"], js_class = "map", static_method_of = Map, js_name = findExit)]
26    fn find_exit(from_room: &JsString, to_room: &JsString, options: &JsValue) -> i8;
27
28    #[wasm_bindgen(js_namespace = ["Game"], js_class = "map", static_method_of = Map, js_name = findRoute)]
29    fn find_route(from_room: &JsString, to_room: &JsString, options: &JsValue) -> JsValue;
30
31    #[wasm_bindgen(js_namespace = ["Game"], js_class = "map", static_method_of = Map, js_name = getRoomLinearDistance)]
32    fn get_room_linear_distance(room_1: &JsString, room_2: &JsString, continuous: bool) -> u32;
33
34    #[wasm_bindgen(js_namespace = ["Game"], js_class = "map", static_method_of = Map, js_name = getRoomTerrain, catch)]
35    fn get_room_terrain(room_name: &JsString) -> Result<RoomTerrain, JsValue>;
36
37    #[wasm_bindgen(js_namespace = ["Game"], js_class = "map", static_method_of = Map, js_name = getWorldSize)]
38    fn get_world_size() -> u32;
39
40    #[wasm_bindgen(js_namespace = ["Game"], js_class = "map", static_method_of = Map, js_name = getRoomStatus, catch)]
41    fn get_room_status(room_name: &JsString) -> Result<JsRoomStatusResult, JsValue>;
42}
43
44/// Get an object with information about the exits from a given room, with
45/// [`JsString`] versions of direction integers as keys and [`JsString`]
46/// room names as values.
47///
48/// [Screeps documentation](https://docs.screeps.com/api/#Game.map.describeExits)
49pub fn describe_exits(room_name: RoomName) -> JsHashMap<Direction, RoomName> {
50    let room_name = room_name.into();
51
52    Map::describe_exits(&room_name).into()
53}
54
55/// Get the distance used for range calculations between two rooms,
56/// optionally setting `continuous` to true to consider the world borders to
57/// wrap around, which is used for terminal calculations.
58///
59/// [Screeps documentation](https://docs.screeps.com/api/#Game.map.getRoomLinearDistance)
60pub fn get_room_linear_distance(from_room: RoomName, to_room: RoomName, continuous: bool) -> u32 {
61    let from_room = from_room.into();
62    let to_room = to_room.into();
63
64    Map::get_room_linear_distance(&from_room, &to_room, continuous)
65}
66
67/// Get the [`RoomTerrain`] object for any room, even one you don't have
68/// vision in.
69///
70/// [Screeps documentation](https://docs.screeps.com/api/#Game.map.getRoomTerrain)
71pub fn get_room_terrain(room_name: RoomName) -> Option<RoomTerrain> {
72    let name = room_name.into();
73
74    Map::get_room_terrain(&name).ok()
75}
76
77/// Get the size of the world map.
78///
79/// [Screeps documentation](https://docs.screeps.com/api/#Game.map.getWorldSize)
80pub fn get_world_size() -> u32 {
81    Map::get_world_size()
82}
83
84#[wasm_bindgen]
85extern "C" {
86    #[wasm_bindgen]
87    pub type JsRoomStatusResult;
88
89    #[wasm_bindgen(method, getter = status)]
90    pub fn status(this: &JsRoomStatusResult) -> RoomStatus;
91
92    #[wasm_bindgen(method, getter = timestamp)]
93    pub fn timestamp(this: &JsRoomStatusResult) -> Option<f64>;
94}
95
96#[derive(Clone, Debug)]
97pub struct RoomStatusResult {
98    status: RoomStatus,
99    timestamp: Option<f64>,
100}
101
102impl RoomStatusResult {
103    pub fn status(&self) -> RoomStatus {
104        self.status
105    }
106
107    pub fn timestamp(&self) -> Option<f64> {
108        self.timestamp
109    }
110}
111
112impl From<JsRoomStatusResult> for RoomStatusResult {
113    fn from(val: JsRoomStatusResult) -> Self {
114        RoomStatusResult {
115            status: val.status(),
116            timestamp: val.timestamp(),
117        }
118    }
119}
120
121#[wasm_bindgen]
122#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Sequence, Deserialize, Serialize)]
123pub enum RoomStatus {
124    Normal = "normal",
125    Closed = "closed",
126    Novice = "novice",
127    Respawn = "respawn",
128}
129
130/// Get the status of a given room, determining whether it's in a special
131/// area or currently inaccessible.
132///
133/// [Screeps documentation](https://docs.screeps.com/api/#Game.map.getRoomStatus)
134pub fn get_room_status(room_name: RoomName) -> Option<RoomStatusResult> {
135    let name = room_name.into();
136
137    Map::get_room_status(&name).ok().map(RoomStatusResult::from)
138}
139
140#[wasm_bindgen]
141extern "C" {
142    /// Object that represents a set of options for a call to [`find_route`].
143    #[wasm_bindgen]
144    pub type JsFindRouteOptions;
145
146    /// Route callback, which determines the cost of entering a given room (the
147    /// first parameter) from a given neighbor room (the second parameter), or
148    /// [`f64::INFINITY`] to block entry into the room.
149    #[wasm_bindgen(method, setter = routeCallback)]
150    pub fn route_callback(
151        this: &JsFindRouteOptions,
152        callback: &Closure<dyn FnMut(JsString, JsString) -> f64>,
153    );
154}
155
156impl JsFindRouteOptions {
157    pub fn new() -> JsFindRouteOptions {
158        Object::new().unchecked_into()
159    }
160}
161
162impl Default for JsFindRouteOptions {
163    fn default() -> Self {
164        Self::new()
165    }
166}
167
168pub struct FindRouteOptions<F>
169where
170    F: FnMut(RoomName, RoomName) -> f64,
171{
172    route_callback: F,
173}
174
175impl<F> FindRouteOptions<F>
176where
177    F: FnMut(RoomName, RoomName) -> f64,
178{
179    pub(crate) fn into_js_options<R>(self, callback: impl Fn(&JsFindRouteOptions) -> R) -> R {
180        let mut raw_callback = self.route_callback;
181
182        let mut owned_callback = move |to_room: RoomName, from_room: RoomName| -> f64 {
183            raw_callback(to_room, from_room)
184        };
185
186        //
187        // Type erased and boxed callback: no longer a type specific to the closure
188        // passed in, now unified as &Fn
189        //
190
191        let callback_type_erased: &mut (dyn FnMut(RoomName, RoomName) -> f64) = &mut owned_callback;
192
193        // Overwrite lifetime of reference so it can be passed to javascript.
194        // It's now pretending to be static data. This should be entirely safe
195        // because we control the only use of it and it remains valid during the
196        // pathfinder callback. This transmute is necessary because "some lifetime
197        // above the current scope but otherwise unknown" is not a valid lifetime.
198        //
199
200        let callback_lifetime_erased: &'static mut (dyn FnMut(RoomName, RoomName) -> f64) =
201            unsafe { std::mem::transmute(callback_type_erased) };
202
203        let boxed_callback = Box::new(move |to_room: JsString, from_room: JsString| -> f64 {
204            let to_room = to_room
205                .try_into()
206                .expect("expected 'to' room name in route callback");
207            let from_room = from_room
208                .try_into()
209                .expect("expected 'rom' room name in route callback");
210
211            callback_lifetime_erased(to_room, from_room)
212        }) as Box<dyn FnMut(JsString, JsString) -> f64>;
213
214        let closure = Closure::wrap(boxed_callback);
215
216        //
217        // Create JS object and set properties.
218        //
219
220        let js_options = JsFindRouteOptions::new();
221
222        js_options.route_callback(&closure);
223
224        callback(&js_options)
225    }
226}
227
228impl Default for FindRouteOptions<fn(RoomName, RoomName) -> f64> {
229    fn default() -> Self {
230        const fn room_cost(_to_room: RoomName, _from_room: RoomName) -> f64 {
231            1.0
232        }
233
234        FindRouteOptions {
235            route_callback: room_cost,
236        }
237    }
238}
239
240impl FindRouteOptions<fn(RoomName, RoomName) -> f64> {
241    #[inline]
242    pub fn new() -> Self {
243        Self::default()
244    }
245}
246
247impl<F> FindRouteOptions<F>
248where
249    F: FnMut(RoomName, RoomName) -> f64,
250{
251    pub fn room_callback<F2>(self, route_callback: F2) -> FindRouteOptions<F2>
252    where
253        F2: FnMut(RoomName, RoomName) -> f64,
254    {
255        let FindRouteOptions { route_callback: _ } = self;
256
257        FindRouteOptions { route_callback }
258    }
259}
260
261#[derive(Debug, Deserialize)]
262pub struct RouteStep {
263    pub exit: ExitDirection,
264    pub room: RoomName,
265}
266
267/// Get the route from a given room leading toward a destination room, with
268/// an optional [`FindRouteOptions`] parameter allowing control over the
269/// costs to enter rooms.
270///
271/// Returns an [`Array`] with an object per room in the route, with keys
272/// `exit` containing an [`ExitDirection`] and `room` containing room name
273/// as a [`JsString`].
274///
275/// [Screeps documentation](https://docs.screeps.com/api/#Game.map.findRoute)
276pub fn find_route<F>(
277    from: RoomName,
278    to: RoomName,
279    options: Option<FindRouteOptions<F>>,
280) -> Result<Vec<RouteStep>, ErrorCode>
281where
282    F: FnMut(RoomName, RoomName) -> f64,
283{
284    let from: JsString = from.into();
285    let to: JsString = to.into();
286
287    let result = if let Some(options) = options {
288        options.into_js_options(|js_options| Map::find_route(&from, &to, js_options))
289    } else {
290        Map::find_route(&from, &to, &JsValue::UNDEFINED)
291    };
292
293    if result.is_object() {
294        let result: &Array = result.unchecked_ref();
295
296        let steps: Vec<RouteStep> = result
297            .iter()
298            .map(|step| serde_wasm_bindgen::from_value(step).expect("expected route step"))
299            .collect();
300
301        Ok(steps)
302    } else {
303        // SAFETY: can never be a 0 return from the find_route API function
304        Err(unsafe {
305            ErrorCode::try_result_from_jsvalue(&result)
306                .expect("expected return code for pathing failure")
307                .unwrap_err_unchecked()
308        })
309    }
310}
311
312/// Get the exit direction from a given room leading toward a destination
313/// room, with an optional [`FindRouteOptions`] parameter allowing control
314/// over the costs to enter rooms.
315///
316/// [Screeps documentation](https://docs.screeps.com/api/#Game.map.findExit)
317pub fn find_exit<F>(
318    from: RoomName,
319    to: RoomName,
320    options: Option<FindRouteOptions<F>>,
321) -> Result<ExitDirection, ErrorCode>
322where
323    F: FnMut(RoomName, RoomName) -> f64,
324{
325    let from: JsString = from.into();
326    let to: JsString = to.into();
327
328    let result = if let Some(options) = options {
329        options.into_js_options(|js_options| Map::find_exit(&from, &to, js_options))
330    } else {
331        Map::find_exit(&from, &to, &JsValue::UNDEFINED)
332    };
333
334    if result >= 0 {
335        Ok(ExitDirection::from_i8(result).expect("expected exit direction for pathing"))
336    } else {
337        // SAFETY: can never be an `Ok()` return from `result_from_i8` because
338        // non-negative values are handled by the first branch above
339        Err(unsafe { ErrorCode::result_from_i8(result).unwrap_err_unchecked() })
340    }
341}