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
16pub const LY_PER_VOXEL: f64 = 400.0;
18pub const ROBBINS_CONSTANT: f64 = 0.661_707_182;
20pub const VOXEL_UNCERTAINTY: f64 = ROBBINS_CONSTANT * LY_PER_VOXEL;
23
24pub const SAME_VOXEL_SD: f64 = 0.249_4 * LY_PER_VOXEL;
28
29pub const CROSS_VOXEL_SD: f64 = 0.408_248 * LY_PER_VOXEL;
33
34const PACKED_MASK: u64 = 0xFFFF_FFFF_FFFF;
36
37const PLANET_SHIFT: u32 = 44;
39const SSI_SHIFT: u32 = 32;
40const VOXEL_Y_SHIFT: u32 = 24;
41const VOXEL_Z_SHIFT: u32 = 12;
42
43const MASK_4BIT: u64 = 0xF;
45const MASK_8BIT: u64 = 0xFF;
46const MASK_12BIT: u64 = 0xFFF;
47
48const SIGN_BIT_12: u16 = 0x800;
50const SIGN_EXTEND_12: u16 = 0xF000;
51
52const SB_TO_PORTAL_XZ: u16 = 0x801;
54const SB_TO_PORTAL_Y: u16 = 0x81;
56const PORTAL_TO_SB_XZ: u16 = 0x7FF;
58const PORTAL_TO_SB_Y: u16 = 0x7F;
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
69#[cfg_attr(
70 feature = "archive",
71 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
72)]
73pub struct GalacticAddress {
74 packed: u64,
75 pub reality_index: u8,
76}
77
78impl GalacticAddress {
79 pub fn new(
81 voxel_x: i16,
82 voxel_y: i8,
83 voxel_z: i16,
84 solar_system_index: u16,
85 planet_index: u8,
86 reality_index: u8,
87 ) -> Self {
88 let x_bits = (voxel_x as u16 as u64) & MASK_12BIT;
89 let y_bits = (voxel_y as u8 as u64) & MASK_8BIT;
90 let z_bits = (voxel_z as u16 as u64) & MASK_12BIT;
91 let ssi_bits = (solar_system_index as u64) & MASK_12BIT;
92 let p_bits = (planet_index as u64) & MASK_4BIT;
93
94 let packed = (p_bits << PLANET_SHIFT)
95 | (ssi_bits << SSI_SHIFT)
96 | (y_bits << VOXEL_Y_SHIFT)
97 | (z_bits << VOXEL_Z_SHIFT)
98 | x_bits;
99
100 Self {
101 packed,
102 reality_index,
103 }
104 }
105
106 pub fn from_packed(packed: u64, reality_index: u8) -> Self {
108 Self {
109 packed: packed & PACKED_MASK,
110 reality_index,
111 }
112 }
113
114 pub fn packed(&self) -> u64 {
116 self.packed
117 }
118
119 pub fn planet_index(&self) -> u8 {
121 ((self.packed >> PLANET_SHIFT) & MASK_4BIT) as u8
122 }
123
124 pub fn solar_system_index(&self) -> u16 {
126 ((self.packed >> SSI_SHIFT) & MASK_12BIT) as u16
127 }
128
129 pub fn voxel_y(&self) -> i8 {
131 ((self.packed >> VOXEL_Y_SHIFT) & MASK_8BIT) as u8 as i8
132 }
133
134 pub fn voxel_z(&self) -> i16 {
136 let raw = ((self.packed >> VOXEL_Z_SHIFT) & MASK_12BIT) as u16;
137 if raw & SIGN_BIT_12 != 0 {
138 (raw | SIGN_EXTEND_12) as i16
139 } else {
140 raw as i16
141 }
142 }
143
144 pub fn voxel_x(&self) -> i16 {
146 let raw = (self.packed & MASK_12BIT) as u16;
147 if raw & SIGN_BIT_12 != 0 {
148 (raw | SIGN_EXTEND_12) as i16
149 } else {
150 raw as i16
151 }
152 }
153
154 pub fn voxel_position(&self) -> (i16, i8, i16) {
156 (self.voxel_x(), self.voxel_y(), self.voxel_z())
157 }
158
159 pub fn from_signal_booster(
165 s: &str,
166 planet_index: u8,
167 reality_index: u8,
168 ) -> Result<Self, AddressParseError> {
169 let parts: Vec<&str> = s.split(':').collect();
170 if parts.len() != 4 {
171 return Err(AddressParseError::InvalidFormat);
172 }
173 let sb_x = u16::from_str_radix(parts[0], 16).map_err(|_| AddressParseError::InvalidHex)?;
174 let sb_y = u16::from_str_radix(parts[1], 16).map_err(|_| AddressParseError::InvalidHex)?;
175 let sb_z = u16::from_str_radix(parts[2], 16).map_err(|_| AddressParseError::InvalidHex)?;
176 let ssi = u16::from_str_radix(parts[3], 16).map_err(|_| AddressParseError::InvalidHex)?;
177
178 let portal_x = sb_x.wrapping_add(SB_TO_PORTAL_XZ) & MASK_12BIT as u16;
179 let portal_y = (sb_y.wrapping_add(SB_TO_PORTAL_Y) & MASK_8BIT as u16) as u8;
180 let portal_z = sb_z.wrapping_add(SB_TO_PORTAL_XZ) & MASK_12BIT as u16;
181
182 let packed = ((planet_index as u64 & MASK_4BIT) << PLANET_SHIFT)
183 | ((ssi as u64 & MASK_12BIT) << SSI_SHIFT)
184 | ((portal_y as u64) << VOXEL_Y_SHIFT)
185 | ((portal_z as u64) << VOXEL_Z_SHIFT)
186 | (portal_x as u64);
187
188 Ok(Self {
189 packed,
190 reality_index,
191 })
192 }
193
194 pub fn to_signal_booster(&self) -> String {
199 let portal_x = (self.packed & MASK_12BIT) as u16;
200 let portal_y = ((self.packed >> VOXEL_Y_SHIFT) & MASK_8BIT) as u16;
201 let portal_z = ((self.packed >> VOXEL_Z_SHIFT) & MASK_12BIT) as u16;
202 let ssi = ((self.packed >> SSI_SHIFT) & MASK_12BIT) as u16;
203
204 let sb_x = portal_x.wrapping_add(PORTAL_TO_SB_XZ) & MASK_12BIT as u16;
205 let sb_y = portal_y.wrapping_add(PORTAL_TO_SB_Y) & MASK_8BIT as u16;
206 let sb_z = portal_z.wrapping_add(PORTAL_TO_SB_XZ) & MASK_12BIT as u16;
207
208 format!("{sb_x:04X}:{sb_y:04X}:{sb_z:04X}:{ssi:04X}")
209 }
210
211 pub fn distance_ly(&self, other: &GalacticAddress) -> f64 {
217 if self.same_system(other) {
218 0.0
219 } else if self.same_region(other) {
220 VOXEL_UNCERTAINTY
221 } else {
222 let (x1, y1, z1) = self.voxel_position();
223 let (x2, y2, z2) = other.voxel_position();
224 let dx = (x1 as f64) - (x2 as f64);
225 let dy = (y1 as f64) - (y2 as f64);
226 let dz = (z1 as f64) - (z2 as f64);
227 (dx * dx + dy * dy + dz * dz).sqrt() * LY_PER_VOXEL
228 }
229 }
230
231 pub fn same_region(&self, other: &GalacticAddress) -> bool {
233 self.voxel_x() == other.voxel_x()
234 && self.voxel_y() == other.voxel_y()
235 && self.voxel_z() == other.voxel_z()
236 }
237
238 pub fn same_system(&self, other: &GalacticAddress) -> bool {
240 self.same_region(other) && self.solar_system_index() == other.solar_system_index()
241 }
242
243 pub fn within(&self, other: &GalacticAddress, ly: f64) -> bool {
245 self.distance_ly(other) <= ly
246 }
247
248 pub fn distance_to_core_ly(&self) -> f64 {
253 let (x, y, z) = self.voxel_position();
254 let dx = x as f64;
255 let dy = y as f64;
256 let dz = z as f64;
257 (dx * dx + dy * dy + dz * dz).sqrt() * LY_PER_VOXEL
258 }
259
260 pub fn is_black_hole(&self) -> bool {
262 self.solar_system_index() == SSI_BLACK_HOLE
263 }
264
265 pub fn is_atlas_interface(&self) -> bool {
267 self.solar_system_index() == SSI_ATLAS_INTERFACE
268 }
269
270 pub fn is_purple_system(&self) -> bool {
272 let ssi = self.solar_system_index();
273 (SSI_PURPLE_START..=SSI_PURPLE_END).contains(&ssi)
274 }
275}
276
277impl fmt::Display for GalacticAddress {
279 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280 write!(f, "0x{:012X}", self.packed)
281 }
282}
283
284impl FromStr for GalacticAddress {
287 type Err = AddressParseError;
288
289 fn from_str(s: &str) -> Result<Self, Self::Err> {
290 let hex_str = if let Some(stripped) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X"))
291 {
292 stripped
293 } else {
294 s
295 };
296
297 if hex_str.len() != 12 {
298 return Err(AddressParseError::InvalidLength);
299 }
300
301 let packed = u64::from_str_radix(hex_str, 16).map_err(|_| AddressParseError::InvalidHex)?;
302
303 Ok(Self {
304 packed,
305 reality_index: 0,
306 })
307 }
308}
309
310impl From<u64> for GalacticAddress {
312 fn from(packed: u64) -> Self {
313 Self {
314 packed: packed & PACKED_MASK,
315 reality_index: 0,
316 }
317 }
318}
319
320impl From<GalacticAddress> for u64 {
322 fn from(addr: GalacticAddress) -> u64 {
323 addr.packed
324 }
325}
326
327#[derive(Debug, Clone, PartialEq, Eq, Hash)]
329#[non_exhaustive]
330pub enum AddressParseError {
331 InvalidFormat,
332 InvalidHex,
333 InvalidLength,
334}
335
336impl fmt::Display for AddressParseError {
337 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338 match self {
339 Self::InvalidFormat => write!(f, "invalid address format"),
340 Self::InvalidHex => write!(f, "invalid hex digit in address"),
341 Self::InvalidLength => write!(f, "address has wrong number of digits"),
342 }
343 }
344}
345
346impl std::error::Error for AddressParseError {}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
357pub struct PortalAddress {
358 glyphs: [u8; 12],
359}
360
361#[derive(Debug, Clone, PartialEq, Eq)]
363#[non_exhaustive]
364pub enum PortalParseError {
365 WrongLength(usize),
366 InvalidGlyph(GlyphParseError),
367}
368
369impl fmt::Display for PortalParseError {
370 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
371 match self {
372 Self::WrongLength(n) => write!(f, "expected 12 glyphs, got {n}"),
373 Self::InvalidGlyph(e) => write!(f, "{e}"),
374 }
375 }
376}
377
378impl std::error::Error for PortalParseError {}
379
380impl From<GlyphParseError> for PortalParseError {
381 fn from(e: GlyphParseError) -> Self {
382 Self::InvalidGlyph(e)
383 }
384}
385
386impl PortalAddress {
387 pub fn new(glyphs: [u8; 12]) -> Self {
389 for (i, &g) in glyphs.iter().enumerate() {
390 assert!(g < 16, "glyph[{i}] = {g} is out of range 0-15");
391 }
392 Self { glyphs }
393 }
394
395 pub fn glyph(&self, i: usize) -> Glyph {
397 Glyph::new(self.glyphs[i])
398 }
399
400 pub fn glyphs(&self) -> [Glyph; 12] {
402 let mut out = [Glyph::new(0); 12];
403 for (i, slot) in out.iter_mut().enumerate() {
404 *slot = Glyph::new(self.glyphs[i]);
405 }
406 out
407 }
408
409 pub fn to_hex_string(&self) -> String {
411 self.glyphs.iter().map(|g| format!("{g:X}")).collect()
412 }
413
414 pub fn to_emoji_string(&self) -> String {
416 self.glyphs.iter().map(|&g| Glyph::new(g).emoji()).collect()
417 }
418
419 pub fn to_abbrev_string(&self) -> String {
421 self.glyphs
422 .iter()
423 .map(|&g| Glyph::new(g).abbrev())
424 .collect::<Vec<_>>()
425 .join(":")
426 }
427
428 pub fn parse_mixed(s: &str) -> Result<Self, PortalParseError> {
432 let mut glyphs = Vec::with_capacity(12);
433 let mut remaining = s.trim();
434
435 while !remaining.is_empty() && glyphs.len() < 12 {
436 let (glyph, rest) = parse_next_glyph(remaining)?;
437 glyphs.push(glyph.index());
438 remaining = rest;
439 }
440
441 if glyphs.len() != 12 {
442 return Err(PortalParseError::WrongLength(glyphs.len()));
443 }
444
445 if !remaining.is_empty() {
446 return Err(PortalParseError::WrongLength(13));
447 }
448
449 let mut arr = [0u8; 12];
450 arr.copy_from_slice(&glyphs);
451 Ok(Self { glyphs: arr })
452 }
453
454 pub fn to_galactic_address(&self) -> GalacticAddress {
456 GalacticAddress::from(*self)
457 }
458
459 pub fn from_galactic_address(addr: &GalacticAddress) -> Self {
461 PortalAddress::from(*addr)
462 }
463
464 pub fn from_signal_booster(
466 s: &str,
467 planet_index: u8,
468 reality_index: u8,
469 ) -> Result<Self, AddressParseError> {
470 let addr = GalacticAddress::from_signal_booster(s, planet_index, reality_index)?;
471 Ok(PortalAddress::from(addr))
472 }
473}
474
475impl fmt::Display for PortalAddress {
477 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
478 write!(f, "{}", self.to_hex_string())
479 }
480}
481
482impl FromStr for PortalAddress {
484 type Err = PortalParseError;
485
486 fn from_str(s: &str) -> Result<Self, Self::Err> {
487 Self::parse_mixed(s)
488 }
489}
490
491impl From<PortalAddress> for GalacticAddress {
492 fn from(pa: PortalAddress) -> Self {
498 let g = pa.glyphs;
499
500 let planet_index = g[0];
501 let ssi = ((g[1] as u16) << 8) | ((g[2] as u16) << 4) | (g[3] as u16);
502 let y_raw = (g[4] << 4) | g[5];
503 let z_raw = ((g[6] as u16) << 8) | ((g[7] as u16) << 4) | (g[8] as u16);
504 let x_raw = ((g[9] as u16) << 8) | ((g[10] as u16) << 4) | (g[11] as u16);
505
506 let packed = ((planet_index as u64) << PLANET_SHIFT)
507 | ((ssi as u64) << SSI_SHIFT)
508 | ((y_raw as u64) << VOXEL_Y_SHIFT)
509 | ((z_raw as u64) << VOXEL_Z_SHIFT)
510 | (x_raw as u64);
511
512 GalacticAddress::from_packed(packed, 0)
513 }
514}
515
516impl From<GalacticAddress> for PortalAddress {
517 fn from(addr: GalacticAddress) -> Self {
521 let p = addr.packed();
522 let mut glyphs = [0u8; 12];
523
524 for (i, slot) in glyphs.iter_mut().enumerate() {
525 *slot = ((p >> (44 - i * 4)) & 0xF) as u8;
526 }
527
528 PortalAddress { glyphs }
529 }
530}
531
532impl GalacticAddress {
533 pub fn to_portal_address(&self) -> PortalAddress {
535 PortalAddress::from(*self)
536 }
537
538 pub fn from_portal_string(s: &str) -> Result<Self, PortalParseError> {
541 let pa: PortalAddress = s.parse()?;
542 Ok(GalacticAddress::from(pa))
543 }
544}
545
546#[cfg(test)]
547mod tests {
548 use super::*;
549
550 #[test]
551 fn pack_unpack_roundtrip() {
552 let addr = GalacticAddress::new(-350, 42, 1000, 0x123, 3, 0);
553 assert_eq!(addr.voxel_x(), -350);
554 assert_eq!(addr.voxel_y(), 42);
555 assert_eq!(addr.voxel_z(), 1000);
556 assert_eq!(addr.solar_system_index(), 0x123);
557 assert_eq!(addr.planet_index(), 3);
558 }
559
560 #[test]
561 fn from_portal_hex_string() {
562 let addr: GalacticAddress = "01717D8A4EA2".parse().unwrap();
563 assert_eq!(addr.packed(), 0x01717D8A4EA2);
564 assert_eq!(addr.planet_index(), 0);
565 assert_eq!(addr.solar_system_index(), 0x171);
566 let y_raw = ((0x01717D8A4EA2u64 >> 24) & 0xFF) as u8;
567 assert_eq!(addr.voxel_y(), y_raw as i8);
568 }
569
570 #[test]
571 fn display_as_hex() {
572 let addr = GalacticAddress::from_packed(0x01717D8A4EA2, 0);
573 assert_eq!(format!("{addr}"), "0x01717D8A4EA2");
574 }
575
576 #[test]
577 fn from_u64_and_into_u64() {
578 let packed: u64 = 0x01717D8A4EA2;
579 let addr = GalacticAddress::from(packed);
580 let back: u64 = addr.into();
581 assert_eq!(back, packed);
582 }
583
584 #[test]
585 fn parse_with_0x_prefix() {
586 let addr: GalacticAddress = "0x01717D8A4EA2".parse().unwrap();
587 assert_eq!(addr.packed(), 0x01717D8A4EA2);
588 }
589
590 #[test]
591 fn signal_booster_roundtrip() {
592 let addr = GalacticAddress::new(-350, 42, 1000, 0x123, 3, 0);
593 let sb = addr.to_signal_booster();
594 let addr2 = GalacticAddress::from_signal_booster(&sb, 3, 0).unwrap();
595 assert_eq!(addr.packed(), addr2.packed());
596 }
597
598 #[test]
599 fn signal_booster_format() {
600 let addr = GalacticAddress::from_packed(0x01717D8A4EA2, 0);
601 let sb = addr.to_signal_booster();
602 let parts: Vec<&str> = sb.split(':').collect();
603 assert_eq!(parts.len(), 4);
604 for part in &parts {
605 assert_eq!(part.len(), 4);
606 assert!(u16::from_str_radix(part, 16).is_ok());
607 }
608 }
609
610 #[test]
611 fn special_system_indices() {
612 let bh = GalacticAddress::new(0, 0, 0, 0x079, 0, 0);
613 assert!(bh.is_black_hole());
614 assert!(!bh.is_atlas_interface());
615
616 let atlas = GalacticAddress::new(0, 0, 0, 0x07A, 0, 0);
617 assert!(atlas.is_atlas_interface());
618
619 let purple = GalacticAddress::new(0, 0, 0, 0x400, 0, 0);
620 assert!(purple.is_purple_system());
621 }
622
623 #[test]
624 fn distance_same_address_is_zero() {
625 let addr = GalacticAddress::new(100, 50, 200, 0x123, 0, 0);
626 assert_eq!(addr.distance_ly(&addr), 0.0);
627 }
628
629 #[test]
630 fn distance_one_voxel_x() {
631 let a = GalacticAddress::new(0, 0, 0, 0, 0, 0);
632 let b = GalacticAddress::new(1, 0, 0, 0, 0, 0);
633 assert!((a.distance_ly(&b) - 400.0).abs() < 0.01);
634 }
635
636 #[test]
637 fn same_region_different_ssi() {
638 let a = GalacticAddress::new(100, 50, 200, 0x001, 0, 0);
639 let b = GalacticAddress::new(100, 50, 200, 0x002, 0, 0);
640 assert!(a.same_region(&b));
641 assert!(!a.same_system(&b));
642 }
643
644 #[test]
645 fn same_system_same_everything() {
646 let a = GalacticAddress::new(100, 50, 200, 0x123, 0, 0);
647 let b = GalacticAddress::new(100, 50, 200, 0x123, 5, 0);
648 assert!(a.same_system(&b));
649 }
650
651 #[test]
652 fn within_boundary() {
653 let a = GalacticAddress::new(0, 0, 0, 0, 0, 0);
654 let b = GalacticAddress::new(1, 0, 0, 0, 0, 0);
655 assert!(a.within(&b, 400.0));
656 assert!(!a.within(&b, 399.0));
657 }
658
659 #[test]
660 fn negative_voxel_roundtrip() {
661 let addr = GalacticAddress::new(-2048, -128, -2048, 0, 0, 0);
662 assert_eq!(addr.voxel_x(), -2048);
663 assert_eq!(addr.voxel_y(), -128);
664 assert_eq!(addr.voxel_z(), -2048);
665 }
666
667 #[test]
668 fn serde_roundtrip() {
669 let addr = GalacticAddress::new(-350, 42, 1000, 0x123, 3, 5);
670 let json = serde_json::to_string(&addr).unwrap();
671 let addr2: GalacticAddress = serde_json::from_str(&json).unwrap();
672 assert_eq!(addr, addr2);
673 }
674
675 #[test]
678 fn known_address_hex_to_emoji() {
679 let pa: PortalAddress = "01717D8A4EA2".parse().unwrap();
680 let emoji = pa.to_emoji_string();
681 assert_eq!(
683 emoji,
684 "\u{1F305}\u{1F54A}\u{1F41C}\u{1F54A}\u{1F41C}\u{1F680}\u{1F98B}\u{1F54B}\u{1F31C}\u{1F333}\u{1F54B}\u{1F611}"
685 );
686 }
687
688 #[test]
689 fn known_address_emoji_to_hex() {
690 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}";
691 let pa: PortalAddress = emoji_str.parse().unwrap();
692 assert_eq!(pa.to_hex_string(), "01717D8A4EA2");
693 }
694
695 #[test]
696 fn galactic_address_portal_roundtrip() {
697 let ga = GalacticAddress::from_packed(0x01717D8A4EA2, 0);
698 let pa = PortalAddress::from(ga);
699 assert_eq!(pa.to_hex_string(), "01717D8A4EA2");
700 let ga2 = GalacticAddress::from(pa);
701 assert_eq!(ga.packed(), ga2.packed());
702 }
703
704 #[test]
705 fn hex_string_roundtrip() {
706 let pa: PortalAddress = "01717D8A4EA2".parse().unwrap();
707 let hex = pa.to_hex_string();
708 assert_eq!(hex, "01717D8A4EA2");
709 let pa2: PortalAddress = hex.parse().unwrap();
710 assert_eq!(pa, pa2);
711 }
712
713 #[test]
714 fn full_roundtrip_ga_pa_hex_pa_ga() {
715 let ga1 = GalacticAddress::new(-350, 42, 1000, 0x123, 3, 5);
716 let pa1 = ga1.to_portal_address();
717 let hex = pa1.to_hex_string();
718 let pa2: PortalAddress = hex.parse().unwrap();
719 let ga2 = pa2.to_galactic_address();
720 assert_eq!(ga1.packed(), ga2.packed());
722 }
723
724 #[test]
725 fn parse_emoji_with_variation_selectors() {
726 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}";
727 let pa_with: PortalAddress = with_vs.parse().unwrap();
728
729 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}";
730 let pa_without: PortalAddress = without_vs.parse().unwrap();
731
732 assert_eq!(pa_with, pa_without);
733 }
734
735 #[test]
736 fn parse_mixed_input() {
737 let mixed = "\u{1F305}1\u{1F41C}Bird\u{1F41C}D8A4EA2";
738 let pa: PortalAddress = mixed.parse().unwrap();
739 assert_eq!(pa.to_hex_string(), "01717D8A4EA2");
740 }
741
742 #[test]
743 fn wrong_length_errors() {
744 assert!("0171".parse::<PortalAddress>().is_err());
745 assert!("01717D8A4EA20".parse::<PortalAddress>().is_err());
746 }
747
748 #[test]
749 fn signal_booster_to_portal_address() {
750 let ga = GalacticAddress::new(0, 0, 0, 0x100, 0, 0);
751 let sb = ga.to_signal_booster();
752 let pa = PortalAddress::from_signal_booster(&sb, 0, 0).unwrap();
753 assert_eq!(pa.to_galactic_address().packed(), ga.packed());
754 }
755
756 #[test]
757 fn portal_display_is_hex() {
758 let pa: PortalAddress = "01717D8A4EA2".parse().unwrap();
759 assert_eq!(format!("{pa}"), "01717D8A4EA2");
760 }
761
762 #[test]
765 fn identical_addresses_distance_zero() {
766 let addr = GalacticAddress::new(100, 50, -200, 0x123, 3, 0);
767 assert_eq!(addr.distance_ly(&addr), 0.0);
768 }
769
770 #[test]
771 fn one_voxel_apart_y_axis() {
772 let a = GalacticAddress::new(0, 0, 0, 0x100, 0, 0);
773 let b = GalacticAddress::new(0, 1, 0, 0x100, 0, 0);
774 let dist = a.distance_ly(&b);
775 assert!((dist - 400.0).abs() < 0.001, "expected 400.0, got {}", dist);
776 }
777
778 #[test]
779 fn one_voxel_apart_z_axis() {
780 let a = GalacticAddress::new(0, 0, 0, 0x100, 0, 0);
781 let b = GalacticAddress::new(0, 0, 1, 0x100, 0, 0);
782 let dist = a.distance_ly(&b);
783 assert!((dist - 400.0).abs() < 0.001, "expected 400.0, got {}", dist);
784 }
785
786 #[test]
787 fn diagonal_distance_3_4_5_triangle() {
788 let a = GalacticAddress::new(0, 0, 0, 0x100, 0, 0);
789 let b = GalacticAddress::new(3, 4, 0, 0x100, 0, 0);
790 let dist = a.distance_ly(&b);
791 assert!(
792 (dist - 2000.0).abs() < 0.001,
793 "expected 2000.0, got {}",
794 dist
795 );
796 }
797
798 #[test]
799 fn negative_coordinates_distance() {
800 let a = GalacticAddress::new(-100, -50, -200, 0x100, 0, 0);
801 let b = GalacticAddress::new(100, 50, 200, 0x100, 0, 0);
802 let dist = a.distance_ly(&b);
803 let expected = (210000.0_f64).sqrt() * 400.0;
804 assert!(
805 (dist - expected).abs() < 0.01,
806 "expected {}, got {}",
807 expected,
808 dist
809 );
810 }
811
812 #[test]
813 fn max_distance_across_galaxy() {
814 let a = GalacticAddress::new(-2048, -128, -2048, 0x000, 0, 0);
815 let b = GalacticAddress::new(2047, 127, 2047, 0x000, 0, 0);
816 let dist = a.distance_ly(&b);
817 let expected =
818 ((4095.0_f64).powi(2) + (255.0_f64).powi(2) + (4095.0_f64).powi(2)).sqrt() * 400.0;
819 assert!(
820 (dist - expected).abs() < 1.0,
821 "expected {}, got {}",
822 expected,
823 dist
824 );
825 }
826
827 #[test]
828 fn distance_to_core() {
829 let addr = GalacticAddress::new(3, 4, 0, 0x100, 0, 0);
830 let dist = addr.distance_to_core_ly();
831 assert!(
832 (dist - 2000.0).abs() < 0.001,
833 "expected 2000.0, got {}",
834 dist
835 );
836 }
837
838 #[test]
839 fn distance_to_core_at_origin() {
840 let addr = GalacticAddress::new(0, 0, 0, 0x100, 0, 0);
841 assert_eq!(addr.distance_to_core_ly(), 0.0);
842 }
843
844 #[test]
845 fn different_region() {
846 let a = GalacticAddress::new(100, 50, -200, 0x123, 0, 0);
847 let b = GalacticAddress::new(101, 50, -200, 0x123, 0, 0);
848 assert!(!a.same_region(&b));
849 assert!(!a.same_system(&b));
850 }
851
852 #[test]
853 fn same_region_different_ssi_distance_is_uncertainty() {
854 let a = GalacticAddress::new(100, 50, 200, 0x001, 0, 0);
855 let b = GalacticAddress::new(100, 50, 200, 0x002, 0, 0);
856 assert!(
857 (a.distance_ly(&b) - VOXEL_UNCERTAINTY).abs() < 0.01,
858 "expected ~{}, got {}",
859 VOXEL_UNCERTAINTY,
860 a.distance_ly(&b)
861 );
862 }
863
864 #[test]
865 fn within_zero_distance() {
866 let a = GalacticAddress::new(0, 0, 0, 0x100, 0, 0);
867 assert!(a.within(&a, 0.0));
868 }
869
870 #[test]
871 fn distance_is_symmetric() {
872 let a = GalacticAddress::new(-500, 42, 1000, 0x123, 0, 0);
873 let b = GalacticAddress::new(300, -100, -800, 0x456, 0, 0);
874 assert_eq!(a.distance_ly(&b), b.distance_ly(&a));
875 }
876
877 #[test]
878 fn same_system_new_vs_from_packed_roundtrip() {
879 let player = GalacticAddress::new(100, 50, -200, 42, 2, 0);
881
882 let discovery_packed = GalacticAddress::new(100, 50, -200, 42, 0, 0).packed();
886 let discovery = GalacticAddress::from_packed(discovery_packed, 0);
887
888 assert!(
890 player.same_system(&discovery),
891 "same_system should be true: player={player} (SSI={}, voxel={:?}), \
892 discovery={discovery} (SSI={}, voxel={:?})",
893 player.solar_system_index(),
894 player.voxel_position(),
895 discovery.solar_system_index(),
896 discovery.voxel_position(),
897 );
898
899 assert_eq!(
901 player.distance_ly(&discovery),
902 0.0,
903 "distance_ly should be 0.0 for same system"
904 );
905
906 assert_eq!(player.voxel_x(), discovery.voxel_x());
908 assert_eq!(player.voxel_y(), discovery.voxel_y());
909 assert_eq!(player.voxel_z(), discovery.voxel_z());
910 assert_eq!(player.solar_system_index(), discovery.solar_system_index());
911 assert_ne!(player.planet_index(), discovery.planet_index());
912 }
913
914 #[test]
915 fn same_system_negative_coords_sign_extension_roundtrip() {
916 let player = GalacticAddress::new(-527, -50, 1234, 0x3FF, 5, 0);
918
919 let packed = GalacticAddress::new(-527, -50, 1234, 0x3FF, 0, 0).packed();
921 let discovery = GalacticAddress::from_packed(packed, 0);
922
923 assert_eq!(player.voxel_x(), -527, "voxel_x sign extension failed");
925 assert_eq!(
926 discovery.voxel_x(),
927 -527,
928 "discovery voxel_x sign extension failed"
929 );
930 assert_eq!(player.voxel_y(), -50, "voxel_y sign extension failed");
931 assert_eq!(
932 discovery.voxel_y(),
933 -50,
934 "discovery voxel_y sign extension failed"
935 );
936 assert_eq!(player.voxel_z(), 1234, "voxel_z should remain positive");
937 assert_eq!(
938 discovery.voxel_z(),
939 1234,
940 "discovery voxel_z should remain positive"
941 );
942
943 assert!(
945 player.same_system(&discovery),
946 "same_system should be true for negative coords: player={player}, discovery={discovery}"
947 );
948 assert_eq!(
949 player.distance_ly(&discovery),
950 0.0,
951 "distance_ly should be 0.0 for same system with negative coords"
952 );
953 }
954}