1use js_sys::{Array, JsString, Object};
17use serde::Serialize;
18use wasm_bindgen::prelude::*;
19
20use crate::{
21 local::{Position, RoomName},
22 objects::{CostMatrix, RoomPosition},
23};
24
25#[wasm_bindgen]
26extern "C" {
27 #[wasm_bindgen]
29 pub type PathFinder;
30
31 #[wasm_bindgen(static_method_of = PathFinder, js_name = search)]
39 fn search_internal(origin: &RoomPosition, goal: &JsValue, options: &JsValue) -> SearchResults;
40}
41
42#[wasm_bindgen]
43extern "C" {
44 #[wasm_bindgen]
47 pub type JsSearchOptions;
48
49 #[wasm_bindgen(method, setter = roomCallback)]
52 pub fn room_callback(
53 this: &JsSearchOptions,
54 callback: &Closure<dyn FnMut(JsString) -> JsValue>,
55 );
56
57 #[wasm_bindgen(method, setter = plainCost)]
60 pub fn plain_cost(this: &JsSearchOptions, cost: u8);
61
62 #[wasm_bindgen(method, setter = swampCost)]
65 pub fn swamp_cost(this: &JsSearchOptions, cost: u8);
66
67 #[wasm_bindgen(method, setter = flee)]
70 pub fn flee(this: &JsSearchOptions, val: bool);
71
72 #[wasm_bindgen(method, setter = maxOps)]
75 pub fn max_ops(this: &JsSearchOptions, ops: u32);
76
77 #[wasm_bindgen(method, setter = maxRooms)]
80 pub fn max_rooms(this: &JsSearchOptions, rooms: u8);
81
82 #[wasm_bindgen(method, setter = maxCost)]
84 pub fn max_cost(this: &JsSearchOptions, cost: f64);
85
86 #[wasm_bindgen(method, setter = heuristicWeight)]
89 pub fn heuristic_weight(this: &JsSearchOptions, weight: f64);
90}
91
92impl JsSearchOptions {
93 pub fn new() -> JsSearchOptions {
94 Object::new().unchecked_into()
95 }
96}
97
98impl Default for JsSearchOptions {
99 fn default() -> Self {
100 Self::new()
101 }
102}
103
104#[wasm_bindgen]
105extern "C" {
106 #[wasm_bindgen]
108 pub type SearchResults;
109
110 #[wasm_bindgen(method, getter, js_name = path)]
113 fn path_internal(this: &SearchResults) -> Array;
114
115 #[wasm_bindgen(method, getter)]
117 pub fn ops(this: &SearchResults) -> u32;
118
119 #[wasm_bindgen(method, getter)]
121 pub fn cost(this: &SearchResults) -> u32;
122
123 #[wasm_bindgen(method, getter)]
125 pub fn incomplete(this: &SearchResults) -> bool;
126}
127
128impl SearchResults {
129 pub fn path(&self) -> Vec<Position> {
130 self.path_internal()
131 .iter()
132 .map(|p| p.unchecked_into())
133 .map(|p: RoomPosition| p.into())
134 .collect()
135 }
136
137 pub fn opaque_path(&self) -> Array {
138 self.path_internal()
139 }
140}
141
142pub trait RoomCostResult: Into<JsValue> {}
143
144#[derive(Default)]
145pub enum MultiRoomCostResult {
146 CostMatrix(CostMatrix),
147 Impassable,
148 #[default]
149 Default,
150}
151
152impl RoomCostResult for MultiRoomCostResult {}
153
154impl From<MultiRoomCostResult> for JsValue {
155 fn from(v: MultiRoomCostResult) -> JsValue {
156 match v {
157 MultiRoomCostResult::CostMatrix(m) => m.into(),
158 MultiRoomCostResult::Impassable => JsValue::from_bool(false),
159 MultiRoomCostResult::Default => JsValue::undefined(),
160 }
161 }
162}
163
164#[derive(Default)]
165pub enum SingleRoomCostResult {
166 CostMatrix(CostMatrix),
167 #[default]
168 Default,
169}
170
171impl RoomCostResult for SingleRoomCostResult {}
172
173impl From<SingleRoomCostResult> for JsValue {
174 fn from(v: SingleRoomCostResult) -> JsValue {
175 match v {
176 SingleRoomCostResult::CostMatrix(m) => m.into(),
177 SingleRoomCostResult::Default => JsValue::undefined(),
178 }
179 }
180}
181
182pub struct SearchOptions<F>
183where
184 F: FnMut(RoomName) -> MultiRoomCostResult,
185{
186 callback: F,
187 inner: InnerSearchOptions,
188}
189
190#[derive(Default, Serialize)]
191#[serde(rename_all = "camelCase")]
192struct InnerSearchOptions {
193 plain_cost: Option<u8>,
194 swamp_cost: Option<u8>,
195 flee: Option<bool>,
196 max_ops: Option<u32>,
197 max_rooms: Option<u8>,
198 max_cost: Option<f64>,
199 heuristic_weight: Option<f64>,
200}
201
202impl<F> SearchOptions<F>
203where
204 F: FnMut(RoomName) -> MultiRoomCostResult,
205{
206 pub(crate) fn as_js_options<R>(&mut self, callback: impl Fn(&JsSearchOptions) -> R) -> R {
207 let js_options: JsSearchOptions = serde_wasm_bindgen::to_value(&self.inner)
209 .expect("Unable to serialize search options.")
210 .unchecked_into();
211
212 let boxed_callback: Box<dyn FnMut(JsString) -> JsValue> = Box::new(move |room| {
213 let room = room
214 .try_into()
215 .expect("expected room name in room callback");
216 (self.callback)(room).into()
217 });
218
219 let boxed_callback_lifetime_erased: Box<dyn 'static + FnMut(JsString) -> JsValue> =
225 unsafe { std::mem::transmute(boxed_callback) };
226
227 let closure = Closure::wrap(boxed_callback_lifetime_erased);
228
229 js_options.room_callback(&closure);
230
231 callback(&js_options)
232 }
233}
234
235impl Default for SearchOptions<fn(RoomName) -> MultiRoomCostResult> {
236 fn default() -> Self {
237 fn cost_matrix(_: RoomName) -> MultiRoomCostResult {
238 MultiRoomCostResult::Default
239 }
240
241 SearchOptions {
242 callback: cost_matrix,
243 inner: Default::default(),
244 }
245 }
246}
247
248impl<F> SearchOptions<F>
249where
250 F: FnMut(RoomName) -> MultiRoomCostResult,
251{
252 #[inline]
253 pub fn new(callback: F) -> Self {
254 SearchOptions {
255 callback,
256 inner: Default::default(),
257 }
258 }
259
260 pub fn room_callback<F2>(self, callback: F2) -> SearchOptions<F2>
261 where
262 F2: FnMut(RoomName) -> MultiRoomCostResult,
263 {
264 SearchOptions {
265 callback,
266 inner: self.inner,
267 }
268 }
269
270 #[inline]
272 pub fn plain_cost(mut self, cost: u8) -> Self {
273 self.inner.plain_cost = Some(cost);
274 self
275 }
276
277 #[inline]
279 pub fn swamp_cost(mut self, cost: u8) -> Self {
280 self.inner.swamp_cost = Some(cost);
281 self
282 }
283
284 #[inline]
286 pub fn flee(mut self, flee: bool) -> Self {
287 self.inner.flee = Some(flee);
288 self
289 }
290
291 #[inline]
293 pub fn max_ops(mut self, ops: u32) -> Self {
294 self.inner.max_ops = Some(ops);
295 self
296 }
297
298 #[inline]
300 pub fn max_rooms(mut self, rooms: u8) -> Self {
301 self.inner.max_rooms = Some(rooms);
302 self
303 }
304
305 #[inline]
307 pub fn max_cost(mut self, cost: f64) -> Self {
308 self.inner.max_cost = Some(cost);
309 self
310 }
311
312 #[inline]
314 pub fn heuristic_weight(mut self, weight: f64) -> Self {
315 self.inner.heuristic_weight = Some(weight);
316 self
317 }
318}
319
320#[wasm_bindgen]
321pub struct SearchGoal {
322 pos: Position,
323 range: u32,
324}
325
326impl SearchGoal {
327 pub fn new(pos: Position, range: u32) -> Self {
328 SearchGoal { pos, range }
329 }
330}
331
332#[wasm_bindgen]
333impl SearchGoal {
334 #[wasm_bindgen(getter)]
335 pub fn pos(&self) -> RoomPosition {
336 self.pos.into()
337 }
338
339 #[wasm_bindgen(getter)]
340 pub fn range(&self) -> u32 {
341 self.range
342 }
343}
344
345pub fn search<F>(
346 from: Position,
347 to: Position,
348 range: u32,
349 options: Option<SearchOptions<F>>,
350) -> SearchResults
351where
352 F: FnMut(RoomName) -> MultiRoomCostResult,
353{
354 let goal = SearchGoal { pos: to, range };
355
356 let goal = JsValue::from(goal);
357
358 search_real(from, &goal, options)
359}
360
361pub fn search_many<F>(
362 from: Position,
363 to: impl Iterator<Item = SearchGoal>,
364 options: Option<SearchOptions<F>>,
365) -> SearchResults
366where
367 F: FnMut(RoomName) -> MultiRoomCostResult,
368{
369 let goals: Array = to.map(JsValue::from).collect();
370
371 search_real(from, goals.as_ref(), options)
372}
373
374fn search_real<F>(
375 from: Position,
376 goal: &JsValue,
377 options: Option<SearchOptions<F>>,
378) -> SearchResults
379where
380 F: FnMut(RoomName) -> MultiRoomCostResult,
381{
382 let from = from.into();
383
384 if let Some(mut options) = options {
385 options.as_js_options(|js_options| PathFinder::search_internal(&from, goal, js_options))
386 } else {
387 PathFinder::search_internal(&from, goal, &JsValue::UNDEFINED)
388 }
389}