1use std::{
2 cmp::Ordering,
3 error::Error,
4 fmt::{self, Write},
5 ops,
6 str::FromStr,
7};
8
9use arrayvec::ArrayString;
10use js_sys::JsString;
11use wasm_bindgen::{JsCast, JsValue};
12
13use crate::prelude::*;
14
15use super::{HALF_WORLD_SIZE, VALID_ROOM_NAME_COORDINATES};
16
17#[derive(Copy, Clone, Eq, PartialEq, Hash)]
35pub struct RoomName {
36 packed: u16,
50}
51
52impl fmt::Display for RoomName {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 if cfg!(feature = "sim") && self.packed == 0 {
64 write!(f, "sim")?;
65 return Ok(());
66 }
67
68 let x_coord = self.x_coord();
69 let y_coord = self.y_coord();
70
71 if x_coord >= 0 {
72 write!(f, "E{}", x_coord)?;
73 } else {
74 write!(f, "W{}", -x_coord - 1)?;
75 }
76
77 if y_coord >= 0 {
78 write!(f, "S{}", y_coord)?;
79 } else {
80 write!(f, "N{}", -y_coord - 1)?;
81 }
82
83 Ok(())
84 }
85}
86
87impl fmt::Debug for RoomName {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 f.debug_struct("RoomName")
90 .field("packed", &self.packed)
91 .field("real", &self.to_array_string())
92 .finish()
93 }
94}
95
96impl RoomName {
97 #[inline]
107 pub fn new<T>(x: &T) -> Result<Self, RoomNameParseError>
108 where
109 T: AsRef<str> + ?Sized,
110 {
111 x.as_ref().parse()
112 }
113
114 #[inline]
116 pub const fn from_packed(packed: u16) -> Self {
117 RoomName { packed }
118 }
119
120 pub(super) fn from_coords(x_coord: i32, y_coord: i32) -> Result<Self, RoomNameParseError> {
132 if !VALID_ROOM_NAME_COORDINATES.contains(&x_coord)
133 || !VALID_ROOM_NAME_COORDINATES.contains(&y_coord)
134 {
135 return Err(RoomNameParseError::PositionOutOfBounds { x_coord, y_coord });
136 }
137
138 let room_x = (x_coord + HALF_WORLD_SIZE) as u16;
139 let room_y = (y_coord + HALF_WORLD_SIZE) as u16;
140
141 Ok(Self::from_packed((room_x << 8) | room_y))
142 }
143
144 #[inline]
148 pub const fn x_coord(&self) -> i32 {
149 ((self.packed >> 8) & 0xFF) as i32 - HALF_WORLD_SIZE
150 }
151
152 #[inline]
156 pub const fn y_coord(&self) -> i32 {
157 (self.packed & 0xFF) as i32 - HALF_WORLD_SIZE
158 }
159
160 #[inline]
165 pub const fn packed_repr(&self) -> u16 {
166 self.packed
167 }
168
169 pub fn checked_add(&self, offset: (i32, i32)) -> Option<RoomName> {
178 let (x1, y1) = (self.x_coord(), self.y_coord());
179 let (x2, y2) = offset;
180 let new_x = x1.checked_add(x2)?;
181 let new_y = y1.checked_add(y2)?;
182 Self::from_coords(new_x, new_y).ok()
183 }
184
185 pub fn to_array_string(&self) -> ArrayString<8> {
190 let mut res = ArrayString::new();
191 write!(res, "{self}").expect("expected ArrayString write to be infallible");
192 res
193 }
194}
195
196impl From<RoomName> for JsValue {
197 fn from(name: RoomName) -> JsValue {
198 let array = name.to_array_string();
199
200 JsValue::from_str(array.as_str())
201 }
202}
203
204impl From<&RoomName> for JsValue {
205 fn from(name: &RoomName) -> JsValue {
206 let array = name.to_array_string();
207
208 JsValue::from_str(array.as_str())
209 }
210}
211
212impl From<RoomName> for JsString {
213 fn from(name: RoomName) -> JsString {
214 let val: JsValue = name.into();
215
216 val.unchecked_into()
217 }
218}
219
220impl From<&RoomName> for JsString {
221 fn from(name: &RoomName) -> JsString {
222 let val: JsValue = name.into();
223
224 val.unchecked_into()
225 }
226}
227
228#[derive(Clone, Debug)]
233pub enum RoomNameConversionError {
234 InvalidType,
235 ParseError { err: RoomNameParseError },
236}
237
238impl Error for RoomNameConversionError {}
239
240impl fmt::Display for RoomNameConversionError {
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 match self {
243 RoomNameConversionError::InvalidType => {
244 write!(f, "got invalid input type to room name conversion")
245 }
246 RoomNameConversionError::ParseError { err } => err.fmt(f),
247 }
248 }
249}
250
251impl TryFrom<JsValue> for RoomName {
252 type Error = RoomNameConversionError;
253
254 fn try_from(val: JsValue) -> Result<RoomName, Self::Error> {
255 let val: String = val
256 .as_string()
257 .ok_or(RoomNameConversionError::InvalidType)?;
258
259 RoomName::from_str(&val).map_err(|err| RoomNameConversionError::ParseError { err })
260 }
261}
262
263impl TryFrom<JsString> for RoomName {
264 type Error = <RoomName as FromStr>::Err;
265
266 fn try_from(val: JsString) -> Result<RoomName, Self::Error> {
267 let val: String = val.into();
268
269 RoomName::from_str(&val)
270 }
271}
272
273impl JsCollectionIntoValue for RoomName {
274 fn into_value(self) -> JsValue {
275 self.into()
276 }
277}
278
279impl JsCollectionFromValue for RoomName {
280 fn from_value(val: JsValue) -> Self {
281 let val: JsString = val.unchecked_into();
282 let val: String = val.into();
283
284 RoomName::from_str(&val).expect("expected parseable room name")
285 }
286}
287
288impl ops::Add<(i32, i32)> for RoomName {
289 type Output = Self;
290
291 #[inline]
301 fn add(self, (x, y): (i32, i32)) -> Self {
302 RoomName::from_coords(self.x_coord() + x, self.y_coord() + y)
303 .expect("expected addition to keep RoomName in-bounds")
304 }
305}
306
307impl ops::Sub<(i32, i32)> for RoomName {
308 type Output = Self;
309
310 #[inline]
318 fn sub(self, (x, y): (i32, i32)) -> Self {
319 RoomName::from_coords(self.x_coord() - x, self.y_coord() - y)
320 .expect("expected addition to keep RoomName in-bounds")
321 }
322}
323
324impl ops::Sub<RoomName> for RoomName {
325 type Output = (i32, i32);
326
327 #[inline]
338 fn sub(self, other: RoomName) -> (i32, i32) {
339 (
340 self.x_coord() - other.x_coord(),
341 self.y_coord() - other.y_coord(),
342 )
343 }
344}
345
346impl FromStr for RoomName {
347 type Err = RoomNameParseError;
348
349 fn from_str(s: &str) -> Result<Self, Self::Err> {
350 parse_to_coords(s)
351 .map_err(|()| RoomNameParseError::new(s))
352 .and_then(|(x, y)| RoomName::from_coords(x, y))
353 }
354}
355
356fn parse_to_coords(s: &str) -> Result<(i32, i32), ()> {
357 if cfg!(feature = "sim") && s == "sim" {
358 return Ok((-HALF_WORLD_SIZE, -HALF_WORLD_SIZE));
359 }
360
361 let mut chars = s.char_indices();
362
363 let east = match chars.next() {
364 Some((_, 'E')) | Some((_, 'e')) => true,
365 Some((_, 'W')) | Some((_, 'w')) => false,
366 _ => return Err(()),
367 };
368
369 let (x_coord, south): (i32, bool) = {
370 let (start_index, _) = chars.next().ok_or(())?;
373 let end_index;
374 let south;
375 loop {
376 match chars.next().ok_or(())? {
377 (i, 'N') | (i, 'n') => {
378 end_index = i;
379 south = false;
380 break;
381 }
382 (i, 'S') | (i, 's') => {
383 end_index = i;
384 south = true;
385 break;
386 }
387 _ => continue,
388 }
389 }
390
391 let x_coord = s[start_index..end_index].parse().map_err(|_| ())?;
392
393 (x_coord, south)
394 };
395
396 let y_coord: i32 = {
397 let (start_index, _) = chars.next().ok_or(())?;
398
399 s[start_index..s.len()].parse().map_err(|_| ())?
400 };
401
402 let room_x = if east { x_coord } else { -x_coord - 1 };
403 let room_y = if south { y_coord } else { -y_coord - 1 };
404
405 Ok((room_x, room_y))
406}
407
408#[derive(Clone, Debug)]
413pub enum RoomNameParseError {
414 TooLarge { length: usize },
415 InvalidString { string: ArrayString<8> },
416 PositionOutOfBounds { x_coord: i32, y_coord: i32 },
417}
418
419impl RoomNameParseError {
420 fn new(failed_room_name: &str) -> Self {
422 match ArrayString::from(failed_room_name) {
423 Ok(string) => RoomNameParseError::InvalidString { string },
424 Err(_) => RoomNameParseError::TooLarge {
425 length: failed_room_name.len(),
426 },
427 }
428 }
429}
430
431impl Error for RoomNameParseError {}
432
433impl fmt::Display for RoomNameParseError {
434 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
435 match self {
436 RoomNameParseError::TooLarge { length } => write!(
437 f,
438 "got invalid room name, too large to stick in error. \
439 expected length 8 or less, got length {length}"
440 ),
441 RoomNameParseError::InvalidString { string } => write!(
442 f,
443 "expected room name formatted `[ewEW][0-9]+[nsNS][0-9]+`, found `{string}`"
444 ),
445 RoomNameParseError::PositionOutOfBounds { x_coord, y_coord } => write!(
446 f,
447 "expected room name with coords within -128..+128, found {x_coord}, {y_coord}"
448 ),
449 }
450 }
451}
452
453impl PartialEq<str> for RoomName {
454 fn eq(&self, other: &str) -> bool {
455 let s = self.to_array_string();
456 s.eq_ignore_ascii_case(other)
457 }
458}
459impl PartialEq<RoomName> for str {
460 #[inline]
461 fn eq(&self, other: &RoomName) -> bool {
462 <RoomName as PartialEq<str>>::eq(other, self)
470 }
471}
472
473impl PartialEq<&str> for RoomName {
474 #[inline]
475 fn eq(&self, other: &&str) -> bool {
476 <RoomName as PartialEq<str>>::eq(self, other)
477 }
478}
479
480impl PartialEq<RoomName> for &str {
481 #[inline]
482 fn eq(&self, other: &RoomName) -> bool {
483 <RoomName as PartialEq<str>>::eq(other, self)
484 }
485}
486
487impl PartialEq<String> for RoomName {
488 #[inline]
489 fn eq(&self, other: &String) -> bool {
490 <RoomName as PartialEq<str>>::eq(self, other)
491 }
492}
493
494impl PartialEq<RoomName> for String {
495 #[inline]
496 fn eq(&self, other: &RoomName) -> bool {
497 <RoomName as PartialEq<str>>::eq(other, self)
498 }
499}
500
501impl PartialEq<&String> for RoomName {
502 #[inline]
503 fn eq(&self, other: &&String) -> bool {
504 <RoomName as PartialEq<str>>::eq(self, other)
505 }
506}
507
508impl PartialEq<RoomName> for &String {
509 #[inline]
510 fn eq(&self, other: &RoomName) -> bool {
511 <RoomName as PartialEq<str>>::eq(other, self)
512 }
513}
514
515impl PartialOrd for RoomName {
516 #[inline]
517 fn partial_cmp(&self, other: &RoomName) -> Option<Ordering> {
518 Some(self.cmp(other))
519 }
520}
521
522impl Ord for RoomName {
523 fn cmp(&self, other: &Self) -> Ordering {
524 self.y_coord()
525 .cmp(&other.y_coord())
526 .then_with(|| self.x_coord().cmp(&other.x_coord()))
527 }
528}
529
530mod serde {
531 use std::fmt;
532
533 use serde::{
534 de::{Error, Unexpected, Visitor},
535 Deserialize, Deserializer, Serialize, Serializer,
536 };
537
538 use super::RoomName;
539
540 impl Serialize for RoomName {
541 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
542 where
543 S: Serializer,
544 {
545 serializer.serialize_str(&self.to_array_string())
546 }
547 }
548
549 struct RoomNameVisitor;
550
551 impl Visitor<'_> for RoomNameVisitor {
552 type Value = RoomName;
553
554 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
555 formatter.write_str(
556 "room name formatted `(E|W)[0-9]+(N|S)[0-9]+` with both numbers within -128..128",
557 )
558 }
559
560 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
561 where
562 E: Error,
563 {
564 v.parse()
565 .map_err(|_| E::invalid_value(Unexpected::Str(v), &self))
566 }
567 }
568
569 impl<'de> Deserialize<'de> for RoomName {
570 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
571 where
572 D: Deserializer<'de>,
573 {
574 deserializer.deserialize_str(RoomNameVisitor)
575 }
576 }
577}
578
579#[cfg(test)]
580mod test {
581 use crate::RoomName;
582
583 #[test]
584 fn test_string_equality() {
585 use super::RoomName;
586 let top_left_room = if cfg!(feature = "sim") {
587 "sim"
588 } else {
589 "W127N127"
590 };
591 let room_names = vec!["E21N4", "w6S42", "W17s5", "e2n5", top_left_room];
592 for room_name in room_names {
593 assert_eq!(room_name, RoomName::new(room_name).unwrap());
594 assert_eq!(RoomName::new(room_name).unwrap(), room_name);
595 assert_eq!(RoomName::new(room_name).unwrap(), &room_name.to_string());
596 assert_eq!(&room_name.to_string(), RoomName::new(room_name).unwrap());
597 }
598 }
599
600 #[test]
601 fn checked_add() {
602 let w0n0 = RoomName::new("W0N0").unwrap();
603 let e0n0 = RoomName::new("E0N0").unwrap();
604 let e10n75 = RoomName::new("E10N75").unwrap();
605 let w3n53 = RoomName::new("W3N53").unwrap();
606
607 let w127n127 = RoomName::new("W127N127").unwrap();
609 let w127s127 = RoomName::new("W127S127").unwrap();
610 let e127n127 = RoomName::new("E127N127").unwrap();
611 let e127s127 = RoomName::new("E127S127").unwrap();
612
613 let w127n5 = RoomName::new("W127N5").unwrap();
615
616 assert_eq!(w0n0.checked_add((1, 0)), Some(e0n0));
618 assert_eq!(e0n0.checked_add((10, -75)), Some(e10n75));
619 assert_eq!(e10n75.checked_add((-14, 22)), Some(w3n53));
620 assert_eq!(w3n53.checked_add((-124, -74)), Some(w127n127));
621
622 assert_eq!(w127n127.checked_add((127, 127)), Some(w0n0));
623 assert_eq!(w127s127.checked_add((127, -128)), Some(w0n0));
624 assert_eq!(e127n127.checked_add((-128, 127)), Some(w0n0));
625 assert_eq!(e127s127.checked_add((-128, -128)), Some(w0n0));
626 assert_eq!(w127n5.checked_add((127, 5)), Some(w0n0));
627
628 assert_eq!(w127n127.checked_add((-1, 0)), None);
630 assert_eq!(w127n127.checked_add((-10, 10)), None);
631 assert_eq!(w127n127.checked_add((i32::MIN, 0)), None);
632 assert_eq!(w127n127.checked_add((i32::MIN, i32::MAX)), None);
633
634 assert_eq!(w127s127.checked_add((-1, 0)), None);
635 assert_eq!(w127s127.checked_add((-10, 10)), None);
636 assert_eq!(w127s127.checked_add((i32::MIN, 0)), None);
637 assert_eq!(w127s127.checked_add((i32::MIN, i32::MAX)), None);
638
639 assert_eq!(e127n127.checked_add((1, 0)), None);
640 assert_eq!(e127n127.checked_add((-1, -10)), None);
641 assert_eq!(e127n127.checked_add((i32::MIN, 0)), None);
642 assert_eq!(e127n127.checked_add((i32::MIN, i32::MAX)), None);
643
644 assert_eq!(e127s127.checked_add((1, 0)), None);
645 assert_eq!(e127s127.checked_add((-1, 10)), None);
646 assert_eq!(e127s127.checked_add((i32::MIN, 0)), None);
647 assert_eq!(e127s127.checked_add((i32::MIN, i32::MAX)), None);
648
649 assert_eq!(w127n5.checked_add((-1, 0)), None);
650 assert_eq!(w127n5.checked_add((-1, 10)), None);
651 assert_eq!(w127n5.checked_add((i32::MIN, 0)), None);
652 assert_eq!(w127n5.checked_add((i32::MIN, i32::MAX)), None);
653 }
654}