1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::str::FromStr;
4
5use crate::glyph::{Glyph, GlyphParseError, parse_next_glyph};
6
7pub const SSI_BLACK_HOLE: u16 = 0x079;
9pub const SSI_ATLAS_INTERFACE: u16 = 0x07A;
11pub const SSI_PURPLE_START: u16 = 0x3E8;
13pub const SSI_PURPLE_END: u16 = 0x429;
15
16const PACKED_MASK: u64 = 0xFFFF_FFFF_FFFF;
18
19const PLANET_SHIFT: u32 = 44;
21const SSI_SHIFT: u32 = 32;
22const VOXEL_Y_SHIFT: u32 = 24;
23const VOXEL_Z_SHIFT: u32 = 12;
24
25const MASK_4BIT: u64 = 0xF;
27const MASK_8BIT: u64 = 0xFF;
28const MASK_12BIT: u64 = 0xFFF;
29
30const SIGN_BIT_12: u16 = 0x800;
32const SIGN_EXTEND_12: u16 = 0xF000;
33
34const SB_TO_PORTAL_XZ: u16 = 0x801;
36const SB_TO_PORTAL_Y: u16 = 0x81;
38const PORTAL_TO_SB_XZ: u16 = 0x7FF;
40const PORTAL_TO_SB_Y: u16 = 0x7F;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
51#[cfg_attr(
52 feature = "archive",
53 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
54)]
55pub struct GalacticAddress {
56 packed: u64,
57 pub reality_index: u8,
58}
59
60impl GalacticAddress {
61 pub fn new(
63 voxel_x: i16,
64 voxel_y: i8,
65 voxel_z: i16,
66 solar_system_index: u16,
67 planet_index: u8,
68 reality_index: u8,
69 ) -> Self {
70 let x_bits = (voxel_x as u16 as u64) & MASK_12BIT;
71 let y_bits = (voxel_y as u8 as u64) & MASK_8BIT;
72 let z_bits = (voxel_z as u16 as u64) & MASK_12BIT;
73 let ssi_bits = (solar_system_index as u64) & MASK_12BIT;
74 let p_bits = (planet_index as u64) & MASK_4BIT;
75
76 let packed = (p_bits << PLANET_SHIFT)
77 | (ssi_bits << SSI_SHIFT)
78 | (y_bits << VOXEL_Y_SHIFT)
79 | (z_bits << VOXEL_Z_SHIFT)
80 | x_bits;
81
82 Self {
83 packed,
84 reality_index,
85 }
86 }
87
88 pub fn from_packed(packed: u64, reality_index: u8) -> Self {
90 Self {
91 packed: packed & PACKED_MASK,
92 reality_index,
93 }
94 }
95
96 pub fn packed(&self) -> u64 {
98 self.packed
99 }
100
101 pub fn planet_index(&self) -> u8 {
103 ((self.packed >> PLANET_SHIFT) & MASK_4BIT) as u8
104 }
105
106 pub fn solar_system_index(&self) -> u16 {
108 ((self.packed >> SSI_SHIFT) & MASK_12BIT) as u16
109 }
110
111 pub fn voxel_y(&self) -> i8 {
113 ((self.packed >> VOXEL_Y_SHIFT) & MASK_8BIT) as u8 as i8
114 }
115
116 pub fn voxel_z(&self) -> i16 {
118 let raw = ((self.packed >> VOXEL_Z_SHIFT) & MASK_12BIT) as u16;
119 if raw & SIGN_BIT_12 != 0 {
120 (raw | SIGN_EXTEND_12) as i16
121 } else {
122 raw as i16
123 }
124 }
125
126 pub fn voxel_x(&self) -> i16 {
128 let raw = (self.packed & MASK_12BIT) as u16;
129 if raw & SIGN_BIT_12 != 0 {
130 (raw | SIGN_EXTEND_12) as i16
131 } else {
132 raw as i16
133 }
134 }
135
136 pub fn voxel_position(&self) -> (i16, i8, i16) {
138 (self.voxel_x(), self.voxel_y(), self.voxel_z())
139 }
140
141 pub fn from_signal_booster(
147 s: &str,
148 planet_index: u8,
149 reality_index: u8,
150 ) -> Result<Self, AddressParseError> {
151 let parts: Vec<&str> = s.split(':').collect();
152 if parts.len() != 4 {
153 return Err(AddressParseError::InvalidFormat);
154 }
155 let sb_x = u16::from_str_radix(parts[0], 16).map_err(|_| AddressParseError::InvalidHex)?;
156 let sb_y = u16::from_str_radix(parts[1], 16).map_err(|_| AddressParseError::InvalidHex)?;
157 let sb_z = u16::from_str_radix(parts[2], 16).map_err(|_| AddressParseError::InvalidHex)?;
158 let ssi = u16::from_str_radix(parts[3], 16).map_err(|_| AddressParseError::InvalidHex)?;
159
160 let portal_x = sb_x.wrapping_add(SB_TO_PORTAL_XZ) & MASK_12BIT as u16;
161 let portal_y = (sb_y.wrapping_add(SB_TO_PORTAL_Y) & MASK_8BIT as u16) as u8;
162 let portal_z = sb_z.wrapping_add(SB_TO_PORTAL_XZ) & MASK_12BIT as u16;
163
164 let packed = ((planet_index as u64 & MASK_4BIT) << PLANET_SHIFT)
165 | ((ssi as u64 & MASK_12BIT) << SSI_SHIFT)
166 | ((portal_y as u64) << VOXEL_Y_SHIFT)
167 | ((portal_z as u64) << VOXEL_Z_SHIFT)
168 | (portal_x as u64);
169
170 Ok(Self {
171 packed,
172 reality_index,
173 })
174 }
175
176 pub fn to_signal_booster(&self) -> String {
181 let portal_x = (self.packed & MASK_12BIT) as u16;
182 let portal_y = ((self.packed >> VOXEL_Y_SHIFT) & MASK_8BIT) as u16;
183 let portal_z = ((self.packed >> VOXEL_Z_SHIFT) & MASK_12BIT) as u16;
184 let ssi = ((self.packed >> SSI_SHIFT) & MASK_12BIT) as u16;
185
186 let sb_x = portal_x.wrapping_add(PORTAL_TO_SB_XZ) & MASK_12BIT as u16;
187 let sb_y = portal_y.wrapping_add(PORTAL_TO_SB_Y) & MASK_8BIT as u16;
188 let sb_z = portal_z.wrapping_add(PORTAL_TO_SB_XZ) & MASK_12BIT as u16;
189
190 format!("{sb_x:04X}:{sb_y:04X}:{sb_z:04X}:{ssi:04X}")
191 }
192
193 pub fn distance_ly(&self, other: &GalacticAddress) -> f64 {
198 let (x1, y1, z1) = self.voxel_position();
199 let (x2, y2, z2) = other.voxel_position();
200 let dx = (x1 as f64) - (x2 as f64);
201 let dy = (y1 as f64) - (y2 as f64);
202 let dz = (z1 as f64) - (z2 as f64);
203 (dx * dx + dy * dy + dz * dz).sqrt() * 400.0
204 }
205
206 pub fn same_region(&self, other: &GalacticAddress) -> bool {
208 self.voxel_x() == other.voxel_x()
209 && self.voxel_y() == other.voxel_y()
210 && self.voxel_z() == other.voxel_z()
211 }
212
213 pub fn same_system(&self, other: &GalacticAddress) -> bool {
215 self.same_region(other) && self.solar_system_index() == other.solar_system_index()
216 }
217
218 pub fn within(&self, other: &GalacticAddress, ly: f64) -> bool {
220 self.distance_ly(other) <= ly
221 }
222
223 pub fn distance_to_core_ly(&self) -> f64 {
228 let (x, y, z) = self.voxel_position();
229 let dx = x as f64;
230 let dy = y as f64;
231 let dz = z as f64;
232 (dx * dx + dy * dy + dz * dz).sqrt() * 400.0
233 }
234
235 pub fn is_black_hole(&self) -> bool {
237 self.solar_system_index() == SSI_BLACK_HOLE
238 }
239
240 pub fn is_atlas_interface(&self) -> bool {
242 self.solar_system_index() == SSI_ATLAS_INTERFACE
243 }
244
245 pub fn is_purple_system(&self) -> bool {
247 let ssi = self.solar_system_index();
248 (SSI_PURPLE_START..=SSI_PURPLE_END).contains(&ssi)
249 }
250}
251
252impl fmt::Display for GalacticAddress {
254 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255 write!(f, "0x{:012X}", self.packed)
256 }
257}
258
259impl FromStr for GalacticAddress {
262 type Err = AddressParseError;
263
264 fn from_str(s: &str) -> Result<Self, Self::Err> {
265 let hex_str = if let Some(stripped) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X"))
266 {
267 stripped
268 } else {
269 s
270 };
271
272 if hex_str.len() != 12 {
273 return Err(AddressParseError::InvalidLength);
274 }
275
276 let packed = u64::from_str_radix(hex_str, 16).map_err(|_| AddressParseError::InvalidHex)?;
277
278 Ok(Self {
279 packed,
280 reality_index: 0,
281 })
282 }
283}
284
285impl From<u64> for GalacticAddress {
287 fn from(packed: u64) -> Self {
288 Self {
289 packed: packed & PACKED_MASK,
290 reality_index: 0,
291 }
292 }
293}
294
295impl From<GalacticAddress> for u64 {
297 fn from(addr: GalacticAddress) -> u64 {
298 addr.packed
299 }
300}
301
302#[derive(Debug, Clone, PartialEq, Eq, Hash)]
304#[non_exhaustive]
305pub enum AddressParseError {
306 InvalidFormat,
307 InvalidHex,
308 InvalidLength,
309}
310
311impl fmt::Display for AddressParseError {
312 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313 match self {
314 Self::InvalidFormat => write!(f, "invalid address format"),
315 Self::InvalidHex => write!(f, "invalid hex digit in address"),
316 Self::InvalidLength => write!(f, "address has wrong number of digits"),
317 }
318 }
319}
320
321impl std::error::Error for AddressParseError {}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
332pub struct PortalAddress {
333 glyphs: [u8; 12],
334}
335
336#[derive(Debug, Clone, PartialEq, Eq)]
338#[non_exhaustive]
339pub enum PortalParseError {
340 WrongLength(usize),
341 InvalidGlyph(GlyphParseError),
342}
343
344impl fmt::Display for PortalParseError {
345 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
346 match self {
347 Self::WrongLength(n) => write!(f, "expected 12 glyphs, got {n}"),
348 Self::InvalidGlyph(e) => write!(f, "{e}"),
349 }
350 }
351}
352
353impl std::error::Error for PortalParseError {}
354
355impl From<GlyphParseError> for PortalParseError {
356 fn from(e: GlyphParseError) -> Self {
357 Self::InvalidGlyph(e)
358 }
359}
360
361impl PortalAddress {
362 pub fn new(glyphs: [u8; 12]) -> Self {
364 for (i, &g) in glyphs.iter().enumerate() {
365 assert!(g < 16, "glyph[{i}] = {g} is out of range 0-15");
366 }
367 Self { glyphs }
368 }
369
370 pub fn glyph(&self, i: usize) -> Glyph {
372 Glyph::new(self.glyphs[i])
373 }
374
375 pub fn glyphs(&self) -> [Glyph; 12] {
377 let mut out = [Glyph::new(0); 12];
378 for (i, slot) in out.iter_mut().enumerate() {
379 *slot = Glyph::new(self.glyphs[i]);
380 }
381 out
382 }
383
384 pub fn to_hex_string(&self) -> String {
386 self.glyphs.iter().map(|g| format!("{g:X}")).collect()
387 }
388
389 pub fn to_emoji_string(&self) -> String {
391 self.glyphs.iter().map(|&g| Glyph::new(g).emoji()).collect()
392 }
393
394 pub fn parse_mixed(s: &str) -> Result<Self, PortalParseError> {
398 let mut glyphs = Vec::with_capacity(12);
399 let mut remaining = s.trim();
400
401 while !remaining.is_empty() && glyphs.len() < 12 {
402 let (glyph, rest) = parse_next_glyph(remaining)?;
403 glyphs.push(glyph.index());
404 remaining = rest;
405 }
406
407 if glyphs.len() != 12 {
408 return Err(PortalParseError::WrongLength(glyphs.len()));
409 }
410
411 if !remaining.is_empty() {
412 return Err(PortalParseError::WrongLength(13));
413 }
414
415 let mut arr = [0u8; 12];
416 arr.copy_from_slice(&glyphs);
417 Ok(Self { glyphs: arr })
418 }
419
420 pub fn to_galactic_address(&self) -> GalacticAddress {
422 GalacticAddress::from(*self)
423 }
424
425 pub fn from_galactic_address(addr: &GalacticAddress) -> Self {
427 PortalAddress::from(*addr)
428 }
429
430 pub fn from_signal_booster(
432 s: &str,
433 planet_index: u8,
434 reality_index: u8,
435 ) -> Result<Self, AddressParseError> {
436 let addr = GalacticAddress::from_signal_booster(s, planet_index, reality_index)?;
437 Ok(PortalAddress::from(addr))
438 }
439}
440
441impl fmt::Display for PortalAddress {
443 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
444 write!(f, "{}", self.to_hex_string())
445 }
446}
447
448impl FromStr for PortalAddress {
450 type Err = PortalParseError;
451
452 fn from_str(s: &str) -> Result<Self, Self::Err> {
453 Self::parse_mixed(s)
454 }
455}
456
457impl From<PortalAddress> for GalacticAddress {
458 fn from(pa: PortalAddress) -> Self {
464 let g = pa.glyphs;
465
466 let planet_index = g[0];
467 let ssi = ((g[1] as u16) << 8) | ((g[2] as u16) << 4) | (g[3] as u16);
468 let y_raw = (g[4] << 4) | g[5];
469 let z_raw = ((g[6] as u16) << 8) | ((g[7] as u16) << 4) | (g[8] as u16);
470 let x_raw = ((g[9] as u16) << 8) | ((g[10] as u16) << 4) | (g[11] as u16);
471
472 let packed = ((planet_index as u64) << PLANET_SHIFT)
473 | ((ssi as u64) << SSI_SHIFT)
474 | ((y_raw as u64) << VOXEL_Y_SHIFT)
475 | ((z_raw as u64) << VOXEL_Z_SHIFT)
476 | (x_raw as u64);
477
478 GalacticAddress::from_packed(packed, 0)
479 }
480}
481
482impl From<GalacticAddress> for PortalAddress {
483 fn from(addr: GalacticAddress) -> Self {
487 let p = addr.packed();
488 let mut glyphs = [0u8; 12];
489
490 for (i, slot) in glyphs.iter_mut().enumerate() {
491 *slot = ((p >> (44 - i * 4)) & 0xF) as u8;
492 }
493
494 PortalAddress { glyphs }
495 }
496}
497
498impl GalacticAddress {
499 pub fn to_portal_address(&self) -> PortalAddress {
501 PortalAddress::from(*self)
502 }
503
504 pub fn from_portal_string(s: &str) -> Result<Self, PortalParseError> {
507 let pa: PortalAddress = s.parse()?;
508 Ok(GalacticAddress::from(pa))
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515
516 #[test]
517 fn pack_unpack_roundtrip() {
518 let addr = GalacticAddress::new(-350, 42, 1000, 0x123, 3, 0);
519 assert_eq!(addr.voxel_x(), -350);
520 assert_eq!(addr.voxel_y(), 42);
521 assert_eq!(addr.voxel_z(), 1000);
522 assert_eq!(addr.solar_system_index(), 0x123);
523 assert_eq!(addr.planet_index(), 3);
524 }
525
526 #[test]
527 fn from_portal_hex_string() {
528 let addr: GalacticAddress = "01717D8A4EA2".parse().unwrap();
529 assert_eq!(addr.packed(), 0x01717D8A4EA2);
530 assert_eq!(addr.planet_index(), 0);
531 assert_eq!(addr.solar_system_index(), 0x171);
532 let y_raw = ((0x01717D8A4EA2u64 >> 24) & 0xFF) as u8;
533 assert_eq!(addr.voxel_y(), y_raw as i8);
534 }
535
536 #[test]
537 fn display_as_hex() {
538 let addr = GalacticAddress::from_packed(0x01717D8A4EA2, 0);
539 assert_eq!(format!("{addr}"), "0x01717D8A4EA2");
540 }
541
542 #[test]
543 fn from_u64_and_into_u64() {
544 let packed: u64 = 0x01717D8A4EA2;
545 let addr = GalacticAddress::from(packed);
546 let back: u64 = addr.into();
547 assert_eq!(back, packed);
548 }
549
550 #[test]
551 fn parse_with_0x_prefix() {
552 let addr: GalacticAddress = "0x01717D8A4EA2".parse().unwrap();
553 assert_eq!(addr.packed(), 0x01717D8A4EA2);
554 }
555
556 #[test]
557 fn signal_booster_roundtrip() {
558 let addr = GalacticAddress::new(-350, 42, 1000, 0x123, 3, 0);
559 let sb = addr.to_signal_booster();
560 let addr2 = GalacticAddress::from_signal_booster(&sb, 3, 0).unwrap();
561 assert_eq!(addr.packed(), addr2.packed());
562 }
563
564 #[test]
565 fn signal_booster_format() {
566 let addr = GalacticAddress::from_packed(0x01717D8A4EA2, 0);
567 let sb = addr.to_signal_booster();
568 let parts: Vec<&str> = sb.split(':').collect();
569 assert_eq!(parts.len(), 4);
570 for part in &parts {
571 assert_eq!(part.len(), 4);
572 assert!(u16::from_str_radix(part, 16).is_ok());
573 }
574 }
575
576 #[test]
577 fn special_system_indices() {
578 let bh = GalacticAddress::new(0, 0, 0, 0x079, 0, 0);
579 assert!(bh.is_black_hole());
580 assert!(!bh.is_atlas_interface());
581
582 let atlas = GalacticAddress::new(0, 0, 0, 0x07A, 0, 0);
583 assert!(atlas.is_atlas_interface());
584
585 let purple = GalacticAddress::new(0, 0, 0, 0x400, 0, 0);
586 assert!(purple.is_purple_system());
587 }
588
589 #[test]
590 fn distance_same_address_is_zero() {
591 let addr = GalacticAddress::new(100, 50, 200, 0x123, 0, 0);
592 assert_eq!(addr.distance_ly(&addr), 0.0);
593 }
594
595 #[test]
596 fn distance_one_voxel_x() {
597 let a = GalacticAddress::new(0, 0, 0, 0, 0, 0);
598 let b = GalacticAddress::new(1, 0, 0, 0, 0, 0);
599 assert!((a.distance_ly(&b) - 400.0).abs() < 0.01);
600 }
601
602 #[test]
603 fn same_region_different_ssi() {
604 let a = GalacticAddress::new(100, 50, 200, 0x001, 0, 0);
605 let b = GalacticAddress::new(100, 50, 200, 0x002, 0, 0);
606 assert!(a.same_region(&b));
607 assert!(!a.same_system(&b));
608 }
609
610 #[test]
611 fn same_system_same_everything() {
612 let a = GalacticAddress::new(100, 50, 200, 0x123, 0, 0);
613 let b = GalacticAddress::new(100, 50, 200, 0x123, 5, 0);
614 assert!(a.same_system(&b));
615 }
616
617 #[test]
618 fn within_boundary() {
619 let a = GalacticAddress::new(0, 0, 0, 0, 0, 0);
620 let b = GalacticAddress::new(1, 0, 0, 0, 0, 0);
621 assert!(a.within(&b, 400.0));
622 assert!(!a.within(&b, 399.0));
623 }
624
625 #[test]
626 fn negative_voxel_roundtrip() {
627 let addr = GalacticAddress::new(-2048, -128, -2048, 0, 0, 0);
628 assert_eq!(addr.voxel_x(), -2048);
629 assert_eq!(addr.voxel_y(), -128);
630 assert_eq!(addr.voxel_z(), -2048);
631 }
632
633 #[test]
634 fn serde_roundtrip() {
635 let addr = GalacticAddress::new(-350, 42, 1000, 0x123, 3, 5);
636 let json = serde_json::to_string(&addr).unwrap();
637 let addr2: GalacticAddress = serde_json::from_str(&json).unwrap();
638 assert_eq!(addr, addr2);
639 }
640
641 #[test]
644 fn known_address_hex_to_emoji() {
645 let pa: PortalAddress = "01717D8A4EA2".parse().unwrap();
646 let emoji = pa.to_emoji_string();
647 assert_eq!(
648 emoji,
649 "\u{1F305}\u{1F54A}\u{FE0F}\u{1F41C}\u{1F54A}\u{FE0F}\u{1F41C}\u{1F680}\u{1F98B}\u{1F54B}\u{1F31C}\u{1F333}\u{1F54B}\u{1F611}"
650 );
651 }
652
653 #[test]
654 fn known_address_emoji_to_hex() {
655 let emoji_str = "\u{1F305}\u{1F54A}\u{FE0F}\u{1F41C}\u{1F54A}\u{FE0F}\u{1F41C}\u{1F680}\u{1F98B}\u{1F54B}\u{1F31C}\u{1F333}\u{1F54B}\u{1F611}";
656 let pa: PortalAddress = emoji_str.parse().unwrap();
657 assert_eq!(pa.to_hex_string(), "01717D8A4EA2");
658 }
659
660 #[test]
661 fn galactic_address_portal_roundtrip() {
662 let ga = GalacticAddress::from_packed(0x01717D8A4EA2, 0);
663 let pa = PortalAddress::from(ga);
664 assert_eq!(pa.to_hex_string(), "01717D8A4EA2");
665 let ga2 = GalacticAddress::from(pa);
666 assert_eq!(ga.packed(), ga2.packed());
667 }
668
669 #[test]
670 fn hex_string_roundtrip() {
671 let pa: PortalAddress = "01717D8A4EA2".parse().unwrap();
672 let hex = pa.to_hex_string();
673 assert_eq!(hex, "01717D8A4EA2");
674 let pa2: PortalAddress = hex.parse().unwrap();
675 assert_eq!(pa, pa2);
676 }
677
678 #[test]
679 fn full_roundtrip_ga_pa_hex_pa_ga() {
680 let ga1 = GalacticAddress::new(-350, 42, 1000, 0x123, 3, 5);
681 let pa1 = ga1.to_portal_address();
682 let hex = pa1.to_hex_string();
683 let pa2: PortalAddress = hex.parse().unwrap();
684 let ga2 = pa2.to_galactic_address();
685 assert_eq!(ga1.packed(), ga2.packed());
687 }
688
689 #[test]
690 fn parse_emoji_with_variation_selectors() {
691 let with_vs = "\u{1F305}\u{1F54A}\u{FE0F}\u{1F41C}\u{1F54A}\u{FE0F}\u{1F41C}\u{1F680}\u{1F98B}\u{1F54B}\u{1F31C}\u{1F333}\u{1F54B}\u{1F611}";
692 let pa_with: PortalAddress = with_vs.parse().unwrap();
693
694 let without_vs = "\u{1F305}\u{1F54A}\u{1F41C}\u{1F54A}\u{1F41C}\u{1F680}\u{1F98B}\u{1F54B}\u{1F31C}\u{1F333}\u{1F54B}\u{1F611}";
695 let pa_without: PortalAddress = without_vs.parse().unwrap();
696
697 assert_eq!(pa_with, pa_without);
698 }
699
700 #[test]
701 fn parse_mixed_input() {
702 let mixed = "\u{1F305}1\u{1F41C}Bird\u{1F41C}D8A4EA2";
703 let pa: PortalAddress = mixed.parse().unwrap();
704 assert_eq!(pa.to_hex_string(), "01717D8A4EA2");
705 }
706
707 #[test]
708 fn wrong_length_errors() {
709 assert!("0171".parse::<PortalAddress>().is_err());
710 assert!("01717D8A4EA20".parse::<PortalAddress>().is_err());
711 }
712
713 #[test]
714 fn signal_booster_to_portal_address() {
715 let ga = GalacticAddress::new(0, 0, 0, 0x100, 0, 0);
716 let sb = ga.to_signal_booster();
717 let pa = PortalAddress::from_signal_booster(&sb, 0, 0).unwrap();
718 assert_eq!(pa.to_galactic_address().packed(), ga.packed());
719 }
720
721 #[test]
722 fn portal_display_is_hex() {
723 let pa: PortalAddress = "01717D8A4EA2".parse().unwrap();
724 assert_eq!(format!("{pa}"), "01717D8A4EA2");
725 }
726
727 #[test]
730 fn identical_addresses_distance_zero() {
731 let addr = GalacticAddress::new(100, 50, -200, 0x123, 3, 0);
732 assert_eq!(addr.distance_ly(&addr), 0.0);
733 }
734
735 #[test]
736 fn one_voxel_apart_y_axis() {
737 let a = GalacticAddress::new(0, 0, 0, 0x100, 0, 0);
738 let b = GalacticAddress::new(0, 1, 0, 0x100, 0, 0);
739 let dist = a.distance_ly(&b);
740 assert!((dist - 400.0).abs() < 0.001, "expected 400.0, got {}", dist);
741 }
742
743 #[test]
744 fn one_voxel_apart_z_axis() {
745 let a = GalacticAddress::new(0, 0, 0, 0x100, 0, 0);
746 let b = GalacticAddress::new(0, 0, 1, 0x100, 0, 0);
747 let dist = a.distance_ly(&b);
748 assert!((dist - 400.0).abs() < 0.001, "expected 400.0, got {}", dist);
749 }
750
751 #[test]
752 fn diagonal_distance_3_4_5_triangle() {
753 let a = GalacticAddress::new(0, 0, 0, 0x100, 0, 0);
754 let b = GalacticAddress::new(3, 4, 0, 0x100, 0, 0);
755 let dist = a.distance_ly(&b);
756 assert!(
757 (dist - 2000.0).abs() < 0.001,
758 "expected 2000.0, got {}",
759 dist
760 );
761 }
762
763 #[test]
764 fn negative_coordinates_distance() {
765 let a = GalacticAddress::new(-100, -50, -200, 0x100, 0, 0);
766 let b = GalacticAddress::new(100, 50, 200, 0x100, 0, 0);
767 let dist = a.distance_ly(&b);
768 let expected = (210000.0_f64).sqrt() * 400.0;
769 assert!(
770 (dist - expected).abs() < 0.01,
771 "expected {}, got {}",
772 expected,
773 dist
774 );
775 }
776
777 #[test]
778 fn max_distance_across_galaxy() {
779 let a = GalacticAddress::new(-2048, -128, -2048, 0x000, 0, 0);
780 let b = GalacticAddress::new(2047, 127, 2047, 0x000, 0, 0);
781 let dist = a.distance_ly(&b);
782 let expected =
783 ((4095.0_f64).powi(2) + (255.0_f64).powi(2) + (4095.0_f64).powi(2)).sqrt() * 400.0;
784 assert!(
785 (dist - expected).abs() < 1.0,
786 "expected {}, got {}",
787 expected,
788 dist
789 );
790 }
791
792 #[test]
793 fn distance_to_core() {
794 let addr = GalacticAddress::new(3, 4, 0, 0x100, 0, 0);
795 let dist = addr.distance_to_core_ly();
796 assert!(
797 (dist - 2000.0).abs() < 0.001,
798 "expected 2000.0, got {}",
799 dist
800 );
801 }
802
803 #[test]
804 fn distance_to_core_at_origin() {
805 let addr = GalacticAddress::new(0, 0, 0, 0x100, 0, 0);
806 assert_eq!(addr.distance_to_core_ly(), 0.0);
807 }
808
809 #[test]
810 fn different_region() {
811 let a = GalacticAddress::new(100, 50, -200, 0x123, 0, 0);
812 let b = GalacticAddress::new(101, 50, -200, 0x123, 0, 0);
813 assert!(!a.same_region(&b));
814 assert!(!a.same_system(&b));
815 }
816
817 #[test]
818 fn within_zero_distance() {
819 let a = GalacticAddress::new(0, 0, 0, 0x100, 0, 0);
820 assert!(a.within(&a, 0.0));
821 }
822
823 #[test]
824 fn distance_is_symmetric() {
825 let a = GalacticAddress::new(-500, 42, 1000, 0x123, 0, 0);
826 let b = GalacticAddress::new(300, -100, -800, 0x456, 0, 0);
827 assert_eq!(a.distance_ly(&b), b.distance_ly(&a));
828 }
829}