Skip to main content

nms_core/
player.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::str::FromStr;
4
5use crate::address::GalacticAddress;
6
7/// Maps to PersistentBaseTypes in the save file.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[cfg_attr(
10    feature = "archive",
11    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
12)]
13#[non_exhaustive]
14pub enum BaseType {
15    HomePlanetBase,
16    FreighterBase,
17    ExternalPlanetBase,
18}
19
20impl fmt::Display for BaseType {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        match self {
23            Self::HomePlanetBase => write!(f, "HomePlanetBase"),
24            Self::FreighterBase => write!(f, "FreighterBase"),
25            Self::ExternalPlanetBase => write!(f, "ExternalPlanetBase"),
26        }
27    }
28}
29
30/// Error returned when parsing a base type string fails.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct BaseTypeParseError(pub String);
33
34impl fmt::Display for BaseTypeParseError {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        write!(f, "unknown base type: {}", self.0)
37    }
38}
39
40impl std::error::Error for BaseTypeParseError {}
41
42impl FromStr for BaseType {
43    type Err = BaseTypeParseError;
44
45    fn from_str(s: &str) -> Result<Self, Self::Err> {
46        match s.to_lowercase().as_str() {
47            "homeplanetbase" | "home" => Ok(Self::HomePlanetBase),
48            "freighterbase" | "freighter" => Ok(Self::FreighterBase),
49            "externalplanetbase" | "external" => Ok(Self::ExternalPlanetBase),
50            _ => Err(BaseTypeParseError(s.to_string())),
51        }
52    }
53}
54
55/// A player-owned base at a specific galactic location.
56///
57/// The galaxy (reality index) is encoded in the `address` field.
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59#[cfg_attr(
60    feature = "archive",
61    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
62)]
63#[non_exhaustive]
64pub struct PlayerBase {
65    pub name: String,
66    pub base_type: BaseType,
67    pub address: GalacticAddress,
68    /// Position in local planet coordinates [x, y, z].
69    pub position: [f32; 3],
70    /// Platform-specific user ID.
71    pub owner_uid: Option<String>,
72}
73
74impl PlayerBase {
75    pub fn new(
76        name: String,
77        base_type: BaseType,
78        address: GalacticAddress,
79        position: [f32; 3],
80        owner_uid: Option<String>,
81    ) -> Self {
82        Self {
83            name,
84            base_type,
85            address,
86            position,
87            owner_uid,
88        }
89    }
90
91    /// Galaxy index (convenience accessor for `address.reality_index`).
92    pub fn reality_index(&self) -> u8 {
93        self.address.reality_index
94    }
95}
96
97/// Snapshot of the player's current state from the save file.
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99#[cfg_attr(
100    feature = "archive",
101    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
102)]
103#[non_exhaustive]
104pub struct PlayerState {
105    pub current_address: GalacticAddress,
106    pub current_reality: u8,
107    pub previous_address: Option<GalacticAddress>,
108    pub freighter_address: Option<GalacticAddress>,
109    pub units: u64,
110    pub nanites: u64,
111    pub quicksilver: u64,
112}
113
114impl PlayerState {
115    pub fn new(
116        current_address: GalacticAddress,
117        current_reality: u8,
118        previous_address: Option<GalacticAddress>,
119        freighter_address: Option<GalacticAddress>,
120        units: u64,
121        nanites: u64,
122        quicksilver: u64,
123    ) -> Self {
124        Self {
125            current_address,
126            current_reality,
127            previous_address,
128            freighter_address,
129            units,
130            nanites,
131            quicksilver,
132        }
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn base_type_display_fromstr_roundtrip() {
142        for bt in [
143            BaseType::HomePlanetBase,
144            BaseType::FreighterBase,
145            BaseType::ExternalPlanetBase,
146        ] {
147            let s = bt.to_string();
148            let parsed: BaseType = s.parse().unwrap();
149            assert_eq!(bt, parsed);
150        }
151    }
152
153    #[test]
154    fn base_type_short_names() {
155        assert_eq!(
156            "home".parse::<BaseType>().unwrap(),
157            BaseType::HomePlanetBase
158        );
159        assert_eq!(
160            "freighter".parse::<BaseType>().unwrap(),
161            BaseType::FreighterBase
162        );
163        assert_eq!(
164            "external".parse::<BaseType>().unwrap(),
165            BaseType::ExternalPlanetBase
166        );
167    }
168
169    #[test]
170    fn player_base_reality_index() {
171        let addr = GalacticAddress::new(0, 0, 0, 0, 0, 7);
172        let base = PlayerBase::new(
173            "My Base".into(),
174            BaseType::HomePlanetBase,
175            addr,
176            [1.0, 2.0, 3.0],
177            None,
178        );
179        assert_eq!(base.reality_index(), 7);
180    }
181
182    #[test]
183    fn player_state_constructor() {
184        let addr = GalacticAddress::new(0, 0, 0, 0, 0, 0);
185        let state = PlayerState::new(addr, 0, None, None, 1_000_000, 5000, 200);
186        assert_eq!(state.units, 1_000_000);
187        assert_eq!(state.nanites, 5000);
188        assert_eq!(state.quicksilver, 200);
189    }
190}