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